Guide 29 of 53
Async & Concurrency
async/await, spawn and every for asynchronous and concurrent programming.
On this page
Zolo provides first-class support for asynchronous programming through async/await, and structured concurrency via spawn and every.
Async Functions¶
Declare an async function with the async keyword:
async fn fetch_data(url: str) -> str {
let response = await http.get(url)
return response.body
}Async functions return a promise/future that can be awaited.
Await¶
Use await inside an async function to pause execution until a future resolves:
async fn greet_user(id: int) -> str {
let user = await fetch_user(id)
let profile = await fetch_profile(user.name)
return "Hello, {user.name}! Bio: {profile.bio}"
}await can be used on any expression that returns an async value:
async fn main() {
let result = await greet_user(42)
print(result)
}Await deadlines¶
Use within to limit an await and receive a typed timeout result:
async fn load() -> int {
sleep 20ms
42
}
let outcome = await load() within 1s
match outcome {
.Ok(value) => print(value),
.Err(_) => print("timeout"),
}The outcome is Result<T, std::async::TimeoutError>. A Result returned by
the task remains inside Ok; its own error does not become a timeout. Ordinary
task rejection or panic retains ordinary await failure behavior.
Group the deadline before applying ?: (await load() within 1s)?.
The operand and interval evaluate once, in that order. The deadline starts
after those evaluations and interval conversion. Use a temporal literal, an
admitted temporal unit value or the standard Duration value; bare numeric
values and erased types cannot supply a temporal contract. Intervals must be
nonnegative. Zero allows an already completed value to succeed; a pending
producer receives an immediate deadline request. Positive exact intervals
round up once to the next nanosecond at the monotonic clock boundary.
On expiry, cancellation reaches the producer's suspension checkpoints, and the await waits for its cleanup and owned work before returning the timeout. Cancellation is cooperative: code that never yields cannot be interrupted, and blocking host work may take its configured timeout to finish cleanup. A pending external future needs a supported cancellation and cleanup adapter; an unavailable adapter raises an ordinary failure rather than a timeout.
Buffered fetch deadlines use the desktop VM adapter. Streaming fetch futures currently report an unavailable-adapter failure, including after headers arrive. The WebAssembly Playground runtime currently lacks the host deadline clock and reports an ordinary unavailable-clock failure.
Foreground TaskGroup ownership is separate from an await deadline. Streaming
fetches can join a foreground group: the group retains the HTTP worker through
response-body exit, even after the header Future settles. Cancellation shuts
down the socket and drains the worker. BodyStream.close() closes an abandoned
body explicitly; DNS resolution may remain pending until the system returns.
BodyStream.read_chunk() and read_lines() return validated UTF8 text and
preserve characters split between packets. read_bytes() returns exact binary
chunks as Future<[u8]?>. Response.bytes() and array_buffer() now return
Future<[u8]>; code expecting a byte string should use a numeric byte array or
an explicit validated text conversion. This applies to VM and native/LLVM.
Sequential vs Parallel Awaiting¶
By default, multiple await calls run sequentially:
async fn sequential() {
let a = await task_a() // waits for A first
let b = await task_b() // then waits for B
}To run tasks in parallel, use spawn:
async fn parallel() {
let handle_a = spawn task_a()
let handle_b = spawn task_b()
let a = await handle_a
let b = await handle_b
print("Both done: {a}, {b}")
}Named results from parallel tasks¶
Use one label per task to keep differently typed results together:
let data = parallel {
user = await load_user()
orders = await load_orders()
}
print(data.user.name)
print(data.orders.len())Each right-hand expression belongs to a separate task. All tasks see the enclosing scope, and the labels become fields after the tasks finish. A task cannot read another task's result through its label. Use an explicit scope and channels for communication between tasks. On failure, structured scope cancellation and cleanup complete before the failure propagates. Cancellation is cooperative; tasks must reach a channel, sleep, await or yield checkpoint.
Spawn¶
spawn launches a coroutine concurrently and returns a handle:
// Spawn an expression
let handle = spawn some_async_fn()
// Spawn a block
let handle2 = spawn {
let data = fetch_data("https://api.example.com")
process(data)
}
// Await the result later
let result = await handle
let result2 = await handle2Fire-and-forget¶
You can spawn without awaiting for background tasks:
spawn log_to_server("user_logged_in")
// continues without waitingEvery¶
every schedules a block to run repeatedly at a given interval. The interval
must be a temporal literal with a unit (s, ms, min, h); bare numbers
are rejected because the unit (ms vs s) is ambiguous.
// Run every second
every 1s {
print("tick!")
}
// Sub-second intervals use `ms`
every 500ms {
check_health()
}
// A variable interval is fine — it carries its unit from the literal.
let interval = 500ms
every interval {
check_health()
}Combining with spawn¶
// Background heartbeat
spawn {
every 5s {
send_heartbeat()
}
}
// Main work continues
let data = await fetch_data(url)
process(data)Async Error Handling¶
Async functions compose naturally with Result:
use std::http
use std::Result
async fn safe_fetch(url: str) -> Result<str, str> {
let response = await http.get(url)
if response.status != 200 {
return Result.Err("HTTP {response.status}")
}
return Result.Ok(response.body)
}
async fn main() {
match await safe_fetch("https://api.example.com") {
Result.Ok(body) => print("Got: {body}"),
Result.Err(e) => print("Error: {e}"),
}
}The ? operator also works in async functions:
async fn pipeline(url: str) -> Result<str, str> {
let raw = await safe_fetch(url)?
let parsed = parse_json(raw)?
return Result.Ok(parsed.title)
}Practical Pattern: Concurrent Fetch¶
use std::Array
async fn fetch_all(urls: [str]) -> [str] {
// Spawn all fetches concurrently
let handles = urls |> Array.map(|url| spawn fetch_data(url))
// Await all results
return handles |> Array.map(|h| await h)
}Key Points¶
| Concept | Syntax | Description |
|---|---|---|
| Async function | async fn name() {} |
Returns a future |
| Await | await expr |
Pauses until future resolves |
| Spawn expression | spawn expr |
Run concurrently, returns handle |
| Spawn block | spawn { ... } |
Run block concurrently |
| Repeating task | every ms { ... } |
Execute block on interval |
Related examples and reference¶
DOCS / FEEDBACK
Did this page leave a question?
Tell us where the explanation lost you. Documentation is part of the language experience.