Skip to content

std.async

std.async provides the canonical Future, structured Scope/Join, timeout, and the explicit bridge for coarse blocking work. Use it for direct await, structured waiting, deadlines, scheduler yields, and deliberately offloading a blocking closure. Resource readiness is an SDK/runtime implementation detail, not a public integer-handle future.

The language recognizes async semantics only on the canonical declarations in this module.

Quick Example

Zynx
import std.async;
import std.io;
import std.time;
import std.mem.{ AllocError };

fn worker(value: i32) -> ^Future<i32> {
	_ = await sleep(time.ns(0));
	return value + 1;
}

fn main() throws(AllocError) {
	let result = await worker(41);
	io.println(try ^"{result}");
}

Contract

  • Futures are cold. Calling a future-returning function captures arguments and returns a suspended future; the async body starts only when awaited or passed to a structured helper.
  • Plain Future<T> is neither Send nor Sync. The sole portable identity is Future<T> where Send, whose producer proves the complete possible in-flight frame portable. There is no Future where Sync/Send + Sync and no runtime witness.
  • Future and Join handles cannot be constructed manually. Create futures with future-returning calls or helpers; scoped Joins come only from Scope.start. Handle fields are private.
  • Future payloads cannot contain checked references. Direct await may pass references to future-returning calls while the reference is live.
  • await accepts Future<T> or scoped Join<T>, starts the work when needed, drives it to completion, moves out the result, and consumes the handle.
  • Future<void> is ordinary typed composition: fallthrough, return;, and return void(); complete with void(), and await yields that zero-sized value without a special future or completion path.
  • Bare future expressions must be consumed by await, async.all, async.race, async.timeout, Scope.start, or explicit std.drop.
  • Stored or structured futures that capture references are rejected. Direct await may pass references.
  • Direct await, structured helpers, and local Scope start accept qualified or unqualified Futures. Parallel start requires a qualified frame and separate Send success/error payloads. Scoped Join<T> cannot escape its Scope.
  • The scheduler is cooperative. Suspension happens only at await, runtime futures such as async.sleep, resource I/O, structured helpers, and Scope joins.
  • The scheduler does not guarantee fairness, starvation freedom, exact poll counts, exact timer/readiness poll counts, cross-runtime ordering, or batching strategy.
  • async.blocking is the sole general bridge from cooperative execution to the bounded blocking pool. It consumes one concrete owned where Send closure; it does not detach work or erase the closure behind a public dynamic callable.
  • Structured work is started with Scope.start. The removed Group.add name does not return as an alias: RFC 0064 reserves collection-like add for one non-positional member, while starting cold work is an execution transition.

Runtime Futures

async.yield() and async.sleep(duration) are fully cold. Constructing or explicitly dropping these futures does not touch the scheduler or timer heap. The runtime action is registered only when the future starts. Their intrinsic runtime state is audited portable, so each returns ^Future<void> where Send.

There is no public readable(integer) or writable(integer) bridge. Typed I/O resources create their own cold futures. Internally those futures may register private readiness state tied to the nominal resource identity and its generation, so close/reuse cannot redirect a stale wait to a different host resource.

Blocking Work

async.blocking moves one concrete, single-use where Send closure to the runtime's dedicated bounded blocking pool and returns a cold region-bound Future for its result:

Zynx
// Normative schema; implementation pending.
fn blocking<T: Send, E: Send, F, region L>(job: ^F in L)
	-> ^Future<T throws(E)> in L
	where F: (^self) -> T throws(E) where Send;
Zynx
// RFC 0063 target; implementation pending.
let result = try await async.blocking(<-job);

Use this bridge for one intentionally coarse operation, such as performing a complete synchronous fs.Dir.walk() inside the closure and materializing only the summary that must cross back. Do not submit every directory entry or tiny read as a separate blocking job.

The closure is consumed exactly once and may start on a different OS thread, so its environment and eventual success/error payloads must satisfy the corresponding Send rules. The result Future retains any valid outer region bound; blocking is not a lifetime-erasure or detached-task escape hatch.

The pool is created lazily and has bounded workers and a bounded admission queue. A cold Future dropped before start does not enqueue the closure. A cancellation request before execution removes an admitted job when possible. After the closure starts, cancellation cannot interrupt arbitrary synchronous user code: the Future, its enclosing Scope, race, or timeout waits until the closure returns and cleanup finishes. Only then is cancellation complete.

The same true-completion rule applies to std.async.fs. Native cancellation may be requested, but memory, loans, handles, and blocking work remain owned until the backend proves quiescence. Cancellation is not a checked error and creates no detached job.

Structured Helpers

async.all, async.race, and async.timeout are cold until awaited.

  • async.all(f1, f2, ...) starts all children concurrently, returns one result position per child including void, and cancels unfinished siblings on the first observed child failure or cancellation. For example, Future<void> plus Future<i32> yields (void, i32); zero children yield void().
  • async.race(f1, f2, ...) starts all children concurrently, returns the first observed completion, and cancels losers. All success types must match.
  • async.timeout(f, duration) returns the child result when the child wins and throws TimeoutError.Expired when the deadline wins.

Structured result selection is deterministic. Children are scanned left to right on each structured poll. If multiple outcomes are visible in the same scan, the lower child slot wins. For async.timeout, the child wins a same-scan tie with the deadline.

Cancellation is lifecycle state, not a checked error value added to every returned error set.

Scope And Join

Open cooperative children with [try] await with async.Scope.local() as tasks { ... }. This complete form is the compiler-known RFC 0009 construct, not an application of ordinary with. tasks.start(future) consumes a cold Future and returns an owned scoped Join. The Join cannot escape the Scope body, and Scope close cancel-and-joins every unfinished child before returning.

Local start accepts both ^Future<T> and ^Future<T> where Send. A borrowed Future retains its region and must still be consumed before that region ends.

Zynx
import std.async;
import std.io;
import std.mem.{ AllocError };

fn child(value: i32) -> ^Future<i32> {
	_ = await yield();
	return value;
}

fn main() throws(AllocError) {
	await with async.Scope.local() as tasks {
		let first_join = tasks.start(child(10));
		let second_join = tasks.start(child(32));

		let first = await <-first_join;
		let second = await <-second_join;

		assert first == 10;
		assert second == 32;
	}

	io.println("done");
}

Await each Join directly with ownership transfer. A fallible child contributes its declared error to the Scope outcome, so the whole construct uses leading try; cancellation remains lifecycle state. tasks.cancel() requests orderly cancellation, and Scope close still joins every child.

Parallel Mode

async.Scope.parallel() has the same lexical lifecycle as Scope.local() but may run children on the worker pool. Use it for independent CPU work and keep Scope.local() for cooperative same-worker work. Every parallel start requires ^Future<T throws(E)> where Send; the producer has already verified captures, suspend-live locals, cleanup, nested awaits, and runtime frame state. The start separately requires T and every checked-error payload in E to be Send for Join transport.

Zynx
import std.async;
import std.io;
import std.mem.{ AllocError };

fn square(value: i32) -> ^Future<i32> where Send {
	_ = await yield();
	return value * value;
}

fn main() throws(AllocError) {
	var total = 0;
	await with async.Scope.parallel() as tasks {
		let two = tasks.start(square(2));
		let three = tasks.start(square(3));

		total = await <-two;
		total = total + await <-three;
	}

	assert total == 13;
	io.println(try ^"{total}");
}

An unqualified Future cannot be strengthened at start. A region-bound qualified Future is accepted only when Scope close joins before its region ends. See Concurrency And Parallelism.

Ownership And Cleanup

Future frames own their captured arguments and locals. Owned values are dropped exactly once when the future is awaited and consumed, explicitly std.dropped, cancelled before it starts, or destroyed by the owning runtime after cancellation or completion.

Dropping an unawaited structured future does not enter child async bodies and does not start cold helper futures. Captured child frames are cancelled and cleaned up by the owning runtime.

Registered user defer bodies are part of async cleanup. Cancellation drives them in LIFO order, and await inside a defer body is allowed to finish before the cancelled task becomes complete.

Runtime allocation strategy, task queue layout, timer wheel/heap structure, and resource-readiness backend, blocking-pool worker count, and admission strategy are unspecified. Public behavior is the cold-future contract, bounded blocking offload, true-completion cancellation, structured cancellation contract, result/error propagation, and documented scheduling boundaries.

API Reference

Types

NameKindPurpose
^Future<T>structCold async value returned by future-returning calls and future helpers.
^Future<T> where SendstructStatically portable cold frame; same runtime representation, with no witness.
^Join<T throws(E)>compiler-owned scoped handleStarted child result; must be consumed inside its Scope region.
Scopecompiler-known lexical constructlocal and parallel modes; has no movable user representation.
TimeoutErrorerrorExpired when an explicit timeout wins.

Functions

FunctionSignatureNotes
readyready<T>(value: ^T) -> ^Future<T>Moves a value into an already-ready future.
sleepsleep(duration: time.Duration) -> ^Future<void> where SendCompletes after the timer fires; intrinsic frame state is audited portable.
yieldyield() -> ^Future<void> where SendCooperatively yields to the scheduler; intrinsic frame state is audited portable.
blockingasync.blocking(<-job) -> region-bound cold Future<T throws(E)>Normative schema is given above. Consumes one concrete (^self) -> T throws(E) where Send closure; transported T and every E payload are Send, and cancellation waits for real completion after start.
allall<T...>(futures: ^Future<T>...) -> ^Future<(T...) throws(E)>Structured wait preserving every child position, including void; an empty result pack normalizes to void. E is the union of child error sets.
racerace<T...>(futures: ^Future<T>...) -> ^Future<T throws(E)>Structured race with one success type and unioned child errors.
timeouttimeout<T>(future: ^Future<T throws(E)>, duration: time.Duration) -> ^Future<T throws(E | TimeoutError)>Deadline wrapper; expiry adds TimeoutError.Expired.

Scope Operations

MethodSignatureNotes
Scope.local.startstart(future: ^Future<T throws(E)>) -> ^Join<T throws(E)>Accepts qualified or unqualified identity and starts one local child.
Scope.parallel.startstart<T: Send, E: Send>(future: ^Future<T throws(E)> where Send) -> ^Join<T throws(E)>Requires portable frame plus separate Send success and every concrete error payload.
cancelcancel()Requests orderly cancellation; Scope close still joins all children.

Status

std.async is the current public async API. For language-level semantics, see Async. Use std.async.io for async byte streams, std.channel for async communication, and std.time for durations used by timers and deadlines. For the full exported surface, see Standard Library Inventory.

Released under the MIT License.