Module async/index
Async task utilities — cooperative yield, JoinHandle.join and the
JoinHandle combinators (plans/archive/STD_API_AUDIT.md §7 P0 item 6,
reshaped as futures by plans/ASYNC_IO_API_AUDIT.md A1).
Every combinator (join_all, race, race_first, any, any_first,
timeout) is a FUTURE: it suspends the awaiting task until its handles
are done, so it works the same in main and inside an io.async body.
They take spawned handles, not futures — spawn at the call site:
{ join_all, timeout } :: import("std/async");
{ Duration } :: import("std/time/duration");
h1 := io.spawn(fetch_a(io), io);
h2 := io.spawn(fetch_b(io), io);
handles := ArrayList(JoinHandle(String)).new();
handles.push(h1);
handles.push(h2);
results := io.await(join_all(handles, io), io); // ArrayList(Option(String))
one := io.await(h1.join(io), io); // Option(String), from a task too
For the async-safe Channel and Mutex (usable INSIDE tasks without
parking the event loop the way std/sync's blocking primitives do), see
std/async/channel and std/async/mutex.
Stability
unstable — the names (yield, join, join_all, race, race_first,
any, any_first, timeout, TimeoutError) and the future-returning
shape are settled as of 2026-10-03; the JoinHandle-not-Future argument
type is a decision (plans/ASYNC_IO_API_AUDIT.md Q3). What may still
change is the wait primitive under them (__yo_join_wait_*): today it
wakes on ANY added task, which any works around by re-waiting over the
handles still running.
Types
Why timeout(...) produced no value (D18).
The old Option(T) return collapsed these two outcomes into each other —
and, when T was itself an Option, into a task that legitimately
returned .None. A caller that wants to retry on a slow task but give up
on a cancelled one could not tell them apart.
Variants
| Variant | Fields | Description |
|---|---|---|
Elapsed | The deadline fired first; | |
Aborted | The task was already aborted (or unwound) while the deadline was still unexpired, so it never produced a value. |
Trait Implementations
Methods
clone : (TimeoutError) fn(inout(self) : TimeoutError) -> TimeoutErrorto_string : (TimeoutError) fn(inout(self) : TimeoutError) -> Stringsource : (TimeoutError) fn(inout(self) : TimeoutError) -> Option(dyn(ToString + ))The error that caused this one, or .None at the root of the chain.
Rust's Error::source. Defaulted to .None, so an error with nothing
underneath it implements the trait by saying only what it is; a wrapper
overrides it to hand back what it wrapped. Walking the chain to the root
cause works as of 2026-09-14 — the returned Dyn used to lose the
Error trait on an erased receiver, so a caller could print one link but
not follow it (#521,
issues/fixed/self-trait-in-a-return-type-loses-the-trait-on-an-erased-receiver.md).
Parameters
| Name | Type | Notes |
|---|---|---|
self | TimeoutError |
Functions
Suspend the current async task until the next event-loop turn: drain the ready queue, then poll (and, when idle with pending I/O, block in) I/O.
There is NO TIMER under this. The future is created pending and completed
at the top of the next ready-task drain, after that drain has measured its
budget, so the resumed continuation lands beyond the budget and runs in the
following step — with exactly one __yo_io_poll() in between. That is the
same mechanism std/async/waker's yield_now documents, and the two are
now the same thing.
It was a 1 ms timer until v0.2.32, for a bootstrap reason and not a design
one: yield is on the compiler's OWN import path (through
std/fs/watch), and a seed compiler emits the async runtime it was built
with — so pointing this at __yo_async_yield_start could not link until a
published seed carried that symbol. v0.2.31 is the first that does.
Two earlier shapes are ruled out and neither should come back. A body of
return(()) completes the future SYNCHRONOUSLY, so io.await(yield(io), io)
never reaches __yo_async_poll_step at all: a poll-until-finished loop
built on it (is_finished() + await yield()) spins without ever polling
I/O and the polled task never progresses (the build smoke hang's spin
component, issues/fixed/build-smoke-hangs-registry-perturbation.md). An
ALREADY-COMPLETE future is the same failure by a different route — the
await point takes an inline fast path for one of those and the task never
leaves the C stack.
Parameters
| Name | Type | Notes |
|---|---|---|
io | Io |
Returns: Impl(Future(unit))
Await every handle, in order, resolving to each task's result (.None for
a task that was aborted). All tasks run concurrently on the loop — the wait
costs the SLOWEST task's wall time, not the sum. The handles are not
consumed: a JoinHandle owns a reference to its task and may be awaited
again (it re-reads the same result).
results := io.await(join_all(handles, io), io); // ArrayList(Option(T))
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Resolve once at least one handle is terminal (completed OR aborted), to
its index. The losers keep running: a JoinHandle dropped without an
await DETACHES its task, so abort() the losers if their work is unwanted
(race_first does that for you). Panics on an empty list.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Returns: Impl(Future(usize, Io))
Resolve to the FIRST handle's result and abort the rest — race with the
cleanup done for you (.None if the winner was aborted rather than
completed). Panics on an empty list.
This exists because race/any hand back an index and leave the losers
running: dropping a JoinHandle DETACHES its task (Tokio's semantics,
which is what makes a fire-and-forget io.spawn statement correct), so a
caller who wants the losers' work stopped has to abort() each one. The
original plan prescribed abort-on-drop through Dispose; the handle does
carry a Dispose today (it owns a reference to the task's future), but
abort-on-drop is deliberately not its semantics.
Every loser is aborted AND joined: the join lets a loser suspended in a cancellable operation be released here rather than when that operation would have completed.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Resolve once some handle COMPLETES (not merely aborts), to its index, or
to .None once every handle is terminal without a completion. Like
race, the losers keep running unless aborted (any_first aborts them).
Panics on an empty list.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Resolve to the first handle to COMPLETE and abort the rest — any with
the cleanup done for you. .None when every handle ended without
completing. Panics on an empty list. See race_first for why this exists.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Wait for handle with a deadline: .Ok(v) if it finishes within
limit, .Err(TimeoutError.Elapsed) if the deadline fires first (the
handle is abort()ed), .Err(TimeoutError.Aborted) if the task was
cancelled on its own. The handle stays valid either way (it can be joined
again). Millisecond granularity (the timer contract of std/time/sleep).
r := io.await(timeout(h, Duration.from_millis(i64(200)), io), io);
A task that finishes before the deadline leaves nothing behind: abort()
on the deadline handle deregisters the armed timer in the I/O backend and
releases the deadline task there and then. Until that cancel path existed
the timer kept counting and held ~256 bytes per call for the whole
remaining deadline (issues/fixed/timeout-deadline-timer-future-leak.md).
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handle | JoinHandle(T) | |
limit | Duration | |
io | Io |
Returns: Impl(Future(Result(T, TimeoutError), Io))