Why Async?

Async programming lets your code perform other work while waiting for I/O operations. This is essential for:

  • Network requests (API calls, database queries)
  • File operations
  • AI inference calls
  • Any operation with latency

Async Functions

Mark functions as async to indicate they may suspend:

async-basic.sx
// Async function returns Future<T>
async fn fetch_user(id: String) -> Result<User, HttpError> {
    let response = await http::get("https://api.example.com/users/{id}")
    let user: User = await response.json()
    Ok(user)
}

fn main() {
    // Await the async function
    let user = await fetch_user("123")
    print("Found user: {user.name}")
}

Futures

An async function returns a Future<T> - a value that will be available later. Use await to get the actual value when it's ready.

Running in Parallel

Use parallel to run multiple async operations concurrently:

parallel.sx
async fn fetch_dashboard_data() -> Dashboard {
    // Run all three requests in parallel
    let (user, posts, notifications) = await parallel(
        fetch_user(),
        fetch_posts(),
        fetch_notifications()
    )

    Dashboard { user, posts, notifications }
}

// Parallel with a list of futures
async fn fetch_all_users(ids: List<String>) -> List<User> {
    let futures = ids.map(id => fetch_user(id))
    await parallel(futures)
}

Sequential Await

Sometimes you need to run operations in sequence, where each depends on the previous:

sequential.sx
async fn process_order(order_id: String) -> Result<Receipt, Error> {
    // Step 1: Validate the order
    let order = await fetch_order(order_id)?

    // Step 2: Check inventory (depends on order)
    let inventory = await check_inventory(order.items)?

    // Step 3: Process payment (depends on order)
    let payment = await charge_payment(order.total)?

    // Step 4: Create shipment (depends on all above)
    let shipment = await create_shipment(order, inventory)?

    Ok(Receipt { order, payment, shipment })
}

Timeouts

Prevent operations from hanging indefinitely with timeout:

timeout.sx
async fn fetch_with_timeout(url: String) -> Result<Response, Error> {
    match await timeout(Duration::seconds(5), http::get(url)) {
        Ok(response) => Ok(response),
        Err(TimeoutError) => Err(Error::new("Request timed out"))
    }
}

// Retry with exponential backoff
async fn fetch_with_retry(url: String, max_retries: i64) -> Result<Response, Error> {
    var delay = Duration::ms(100)

    for attempt in 1..=max_retries {
        match await fetch_with_timeout(url.clone()) {
            Ok(response) => return Ok(response),
            Err(e) if attempt < max_retries => {
                print("Attempt {attempt} failed, retrying...")
                await sleep(delay)
                delay = delay * 2  // Exponential backoff
            },
            Err(e) => return Err(e)
        }
    }
}

Select - First to Complete

Use select when you want the result from whichever future completes first:

select.sx
async fn fetch_fastest(urls: List<String>) -> Response {
    let futures = urls.map(url => http::get(url))
    await select(futures)  // Returns first to complete
}

// Race between operation and timeout
async fn with_deadline<T>(
    deadline: Duration,
    operation: Future<T>
) -> Result<T, TimeoutError> {
    await select {
        result = operation => Ok(result),
        _ = sleep(deadline) => Err(TimeoutError)
    }
}

Async in Actors

Actor message handlers can be async:

async-actor.sx
actor DataFetcher {
    var cache: Map<String, Data> = {}

    // Async receive handler
    async receive Fetch(key: String) -> Data {
        // Check cache first
        if let Some(cached) = cache.get(key) {
            return cached
        }

        // Fetch from remote
        let data = await http::get("https://api.example.com/data/{key}")
        let parsed: Data = await data.json()

        // Cache the result
        cache.insert(key, parsed.clone())

        parsed
    }

    async receive FetchMany(keys: List<String>) -> List<Data> {
        // Fetch all in parallel
        let futures = keys.map(key => ask(self, Fetch(key)))
        await parallel(futures)
    }
}

Async Streams

Process sequences of async values with streams:

streams.sx
// Async iterator
async fn process_events(stream: EventStream) {
    // Process each event as it arrives
    for await event in stream {
        match event {
            Event::Message(msg) => handle_message(msg),
            Event::Error(e) => handle_error(e),
            Event::Close => break
        }
    }
}

// Transform a stream
async fn transform_stream(input: Stream<RawData>) -> Stream<Processed> {
    input
        .filter(data => data.is_valid())
        .map(async data => {
            await process(data)
        })
}

Try It Yourself

Build an async web scraper that:

  1. Takes a list of URLs
  2. Fetches them all in parallel with a concurrency limit of 5
  3. Has a 10-second timeout per request
  4. Retries failed requests up to 3 times
  5. Returns the successful responses

Summary

In this tutorial, you learned:

  • Declaring async functions with async fn
  • Awaiting futures with the await keyword
  • Running operations in parallel with parallel
  • Using timeout to prevent hangs
  • Using select to race futures
  • Implementing async message handlers in actors
  • Processing async streams with for await

In the next tutorial, we'll explore error handling with Result, Option, and the ? operator.