spawn
spawn launches a concurrent task that runs independently of the caller. Unlike a direct async call that suspends the current task at the first await, spawn enqueues a new task on the runtime scheduler and returns immediately with a join handle whose result can later be collected with await. It is the primary primitive for structured concurrency alongside async/await.
Ground truth: Lexer
TokenKind::Spawn(src/parsing/lexer.rs:987, keyword"spawn"), runtime schedulersrc/runtime_core/*work-stealing executor, hover docspawn→ Starts a concurrent task without blocking the caller (als/src/hover.rs:367).
Syntax
spawn_expr ::= "spawn" ( block | async_expr | call_expr )
block ::= "{" statements "}"
async_expr ::= "async" block | "async" call_expr
call_expr ::= ident "(" args? ")" | ident "." ident "(" args? ")"
handle ::= awaitable<Task<T>> // value returned by spawn
spawn is a prefix keyword that takes a single operand — typically a block or an async invocation. It is an expression, so it can appear on the right-hand side of let, as an argument, or inside collections.
Canonical forms
let h1 = spawn fetch("https://a.example"); // spawn an async call
let h2 = spawn { heavy_compute(42) }; // spawn a block
let h3 = spawn async { let v = await fetch(url); v.len() }; // spawn async block
// With explicit handle typing (where supported by annotations)
let t: Task<string> = spawn fetch("https://example.com");
let result: string = await t;
Relation to async/await
async fn f(): T — declares suspension points; returns Promise<T>/Task<T>
await expr — suspends current task until expr resolves
spawn expr — creates a sibling task; does NOT suspend caller
spawn + await — concurrent fork then join
Inside an async function, spawn desugars roughly to runtime_core::spawn(future); outside async, some runtimes require spawn to be called from an async context or from main that is itself async.
Semantics
Task model
Each spawn creates an independent task tracked by the executor. Tasks are:
- Concurrent, not parallel-by-default: the work-stealing scheduler may run tasks on the same thread interleaved at
awaitpoints, or on different worker threads whenSend-like constraints allow. CPU-bound blocks withoutawaitstill benefit fromspawnon multi-threaded executors. - Structured: the handle keeps the task alive. Dropping the handle does not cancel the task in the current implementation — the task runs to completion unless the runtime provides explicit cancellation.
- Awaitable:
await handlesuspends the awaiting task until the spawned task completes and yields its return value (or propagates itsthrow).
State-machine interaction with async
async fn bodies are lowered to stackless state machines with Poll::Ready/Poll::Pending transitions (async-concurrency.md:1). spawn submits such a state machine to the executor instead of polling it inline:
caller task: spawn ──► executor queue ──► worker polls spawned task
│ (Waker registered on I/O)
└──► continues without blocking ──► await h ──► suspends until Ready
Scheduling
The runtime (src/runtime_core/) uses a work-stealing deque per worker thread. spawn pushes onto the local queue; idle workers steal tasks, preserving locality while balancing load. spawn is O(1) plus the cost of allocating the future's state.
Error propagation
If the spawned task throws, the exception is captured in the handle. await on that handle re-throws:
async fn failing(): i32 { throw "boom"; }
let h = spawn failing();
try {
let v = await h;
} catch (e) { print(e); } // "boom"
When to use spawn vs direct await
| Pattern | Behavior |
|---|---|
let v = await fetch(a); let w = await fetch(b); | Sequential — b starts after a finishes |
let ha = spawn fetch(a); let hb = spawn fetch(b); let v = await ha; let w = await hb; | Concurrent — both start immediately |
spawn { loop { poll() } } | Background daemon (no join) |
Examples
Example 1 — Concurrent fetch with join
async fn fetch(url: string): string {
// ... asynchronous work (runtime_core I/O) ...
return "body:" + url;
}
async fn main() {
// Fork two independent I/O tasks concurrently
let t1 = spawn fetch("https://a.example");
let t2 = spawn fetch("https://b.example");
// Do other work while they run
print("both fetches in flight");
// Join — suspends until each completes (order independent)
let a = await t1;
let b = await t2;
print(a, b); // "body:https://a.example" "body:https://b.example"
// Collecting results as an array
let handles = [spawn fetch("https://c.example"), spawn fetch("https://d.example")];
let bodies: [string] = [];
for h in handles { bodies.push(await h); }
print(bodies.len()); // 2
}