Module async/index
Async task utilities — cooperative yield and JoinHandle combinators
(plans/archive/STD_API_AUDIT.md §7 P0 item 6).
The combinators (join_all, race, race_first, any, any_first,
timeout) are BLOCKING-POLL
functions in the same sense as JoinHandle.await: they drive the event
loop while they wait, so every spawned task keeps making progress. 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 := join_all(handles, io); // ArrayList(Option(String))
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 and signatures (yield, join_all, race,
race_first, any, any_first,
timeout, TimeoutError) are intended to keep their meanings, but the
MECHANISM under them is expected to change: every combinator drives the
loop by polling, re-checking on a 1 ms timer tick, so a handle that
completes just after a check waits out the rest of the tick. A waker-based
rewrite is planned (plans/STD_API_STABILIZATION.md §4 Concurrency) and
would change the latency and the CPU cost of waiting, not the API. The
JoinHandle-not-Future argument type is a consequence of that design and
may relax with it.
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
impl(generic(T : Type), where(T <: ToString), T : (ToString))
impl(generic(T : Type), where(T <: ToString), T : (ToString), Format)
format : fn(self : Self, spec : str) -> StringRender self under spec. An unrecognised spec degrades to the plain
to_string() rendering rather than failing.
Parameters
| Name | Type | Notes |
|---|---|---|
self | Self | |
spec | str |
Returns: String
Methods
to_string : (TimeoutError) fn(self : TimeoutError) -> Stringsource : (TimeoutError) fn(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 is not expressible yet — the returned Dyn loses the Error
trait on an erased receiver, so a caller can print one link but cannot
follow it (#521,
issues/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/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, returning each task's result (.None for a
task that was aborted). All tasks run concurrently on the loop — awaiting
sequentially costs the SLOWEST task's wall time, not the sum. Each handle
is consumed (a JoinHandle must be awaited at most once).
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Drive the loop until at least one handle is terminal (completed OR
aborted) and return its index. The handles are NOT consumed: every one —
winner and losers alike — must still be awaited exactly once by the
caller (abort the losers first if their results are unwanted). Panics
on an empty list.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Returns: usize
Await the FIRST handle to finish and abort the rest — race with the
cleanup done for you. Returns the winner's result (.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 a manual
contract behind ("every handle must still be awaited exactly once; abort
the losers first if their results are unwanted"), and a caller who forgets
it leaks every loser's state machine. The plan's original prescription for
that leak was a Dispose on JoinHandle that aborts a non-terminal task,
and that is the wrong fix twice over: JoinHandle(T) is a bare copyable
struct over a raw pointer, not an Rc, so it cannot carry Dispose at
all; and abort-on-drop is not the semantics Yo's async JoinHandle
follows — dropping it DETACHES, exactly as Tokio's does, which is what
makes fire-and-forget io.spawn work. The leak is a combinator contract
problem, so it is fixed in the combinator.
Every loser is aborted AND awaited: aborting alone marks the task and leaves its await outstanding, which is the same leak one step later.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Returns: Option(T)
Drive the loop until some handle COMPLETES (not merely aborts) and return
its index, or .None once every handle is terminal without a completion.
Like race, the handles are not consumed — award each exactly one await.
Panics on an empty list.
Type Parameters
| Name | Type | Notes |
|---|---|---|
T | Type | comptime |
Parameters
| Name | Type | Notes |
|---|---|---|
handles | ArrayList(JoinHandle(T)) | |
io | Io |
Returns: Option(usize)
Await 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 |
Returns: Option(T)
Await 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 is consumed either way. Millisecond granularity (the
timer contract of std/time/sleep).
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: Result(T, TimeoutError)