Skip to content

Spawning

Start child computations and control their captures and join order.

Updated View as Markdown

spawn expression starts one child task and produces Task[T], where T is the expression’s type.

task := spawn fetch_booking(id)

The expression is scheduled as child work; it is not evaluated to completion before the Task is returned.

A block is useful for setup or several statements:

task := spawn {
    booking := fetch_booking(id)
    render_booking(booking)
}

The block’s tail expression determines the task result. A block without a value-producing tail yields void.

cleanup_task: Task[void] = spawn {
    remove_expired_entries()
}

Parallel start

Calls remain sequential unless explicitly spawned. Start every child before joining when operations should overlap:

profile_task := spawn { load_profile(user_id) }
orders_task := spawn { load_orders(user_id) }

profile := profile_task.await()
orders := orders_task.await()
return (profile, orders)

This is the current replacement for old spawn.all { ... } examples. The surface spawn.all form was removed.

Starting tasks does not promise which child runs first. Only dependency edges created by messages, selection, or awaiting constrain visible completion order.

Captures

A spawned block may reference values from its enclosing function:

fn schedule(id: int): Task[Booking] {
    prefix := "booking"
    return spawn {
        println("$prefix:$id")
        load_booking(id)
    }
}

The compiler builds a task body with hidden capture parameters. Design code so the child owns or safely shares what it needs. Do not assume assignment inside the child mutates a parent’s copied local binding.

Managed captures are retained for the child and released on completion or cancellation. Raw pointers and borrows cannot freely escape across task and suspension boundaries.

Capture the smallest stable value needed by the child:

user_id := request.user.id
task := spawn {
    load_profile(user_id)
}

This avoids keeping the full request graph alive solely for one field. Shared resource handles such as Stream[T] intentionally alias their control block across copies; ordinary value captures should not be assumed to provide shared mutable state.

Errors

The spawned body is checked like another function body. It must handle or return its typed errors according to its own result contract. When the body returns a Result, awaiting the task returns that result:

task := spawn { load_profile(user_id) }
profile := task.await()?

Discarding a fallible task also discards the source-level place where its result could be observed. Do that only when the child handles failures internally.

Nesting

A child can spawn its own children. These descendants remain in the same structured task tree. Canceling an ancestor propagates through the subtree, and runtime shutdown does not turn discarded handles into detached background processes.

Fire-and-forget

Discarding a task handle starts work without a source-level join:

spawn { record_metric(event) }

This is still a child in the runtime task tree, not an operating-system process detached from its parent. Use fire-and-forget only when the result is irrelevant, failures have a boundary policy, and parent shutdown may end the work.

Effects

Spawning adds Spawn and Suspend to the enclosing effect row. Effects inside the child body are also analyzed, and the runtime chooses an appropriate frame and continuation layout.

The current checker conservatively allows child-body effects to widen the enclosing inferred row. Explicit public effect rows should account for this until effect frames for spawned bodies are fully separated.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close