Structured Concurrency
Concurrent work stays attached to a lifetime that settles it before returning.
Concurrency is easier to reason about when every child computation has an owner. In Hew, fork creates a child task, await takes that task's result, and the owning function or scope settles unfinished children before its lifetime ends.
Run independent work together
fn subtotal() -> i64 { 32 }
fn shipping() -> i64 { 10 }
fn main() {
let total = scope {
let items = fork subtotal();
let delivery = fork shipping();
(await items) + (await delivery)
};
println(total); // 42
}scopeowns both tasks
fork subtotal()32await itemsfork shipping()10await delivery32 + 10 = 42Scope returns 42
Both child tasks settle before the scope returns its value.
Both calls can make progress independently. Each fork returns a task immediately; each await consumes one task handle and yields its result. Before the scope returns total, it has no remaining child work.
Understand the three parts
scope owns a lifetime
A scope is an expression, so its last expression becomes its value. Normal exit waits for unfinished children. A fault or cancellation cancels affected children, waits for their cleanup, and only then leaves the scope or runs its failure handler.
Every function already provides an implicit task lifetime. An explicit scope creates a narrower boundary when a group of child operations should settle before the surrounding function continues.
fork starts child work
A plain function or actor call still waits, even when written inside a scope. Only fork starts a child task. Arguments and captures are acquired before the child uses them: ordinary data becomes an independent value, while an affine owner transfers to the child.
await takes one result
await Task<T> yields exactly T. If the child returns a Result<T, E>, awaiting it yields that same result for the application to handle; await does not add or flatten a Result. Awaiting a vector of tasks returns values in vector order.
Handle deadlines and cancellation
fn main() {
let answer = scope within 20ms {
sleep(1s);
42
} handle failure {
0
};
println(answer);
}The failure handler runs after the scope's children and cleanup have settled. It handles a structured deadline or fault, not an ordinary Result.Err. Parent cancellation keeps propagating outward; an inner handler cannot clear it.
Cancellation is cooperative. Suspending calls, task waits, loop headers, and compiler-inserted safepoints observe it. Long external calls and straight-line code without a safepoint can delay cancellation. The scope still waits for cancelled children to drain, including their resource cleanup.
select waits for readiness
select chooses one ready task, actor call, stream receive, or timer arm. A losing task arm keeps running and remains owned by its scope; selection alone does not cancel it. Use a deadline around the work or race when the operation's contract should cancel and clean up losing work.
For all-of work, use batch fork or await a vector of tasks. See Select and Join for side-by-side patterns.
Keep task and actor lifetimes distinct
| Task | Actor | |
|---|---|---|
| Created by | fork | spawn |
| Lifetime | Bound to its owning function or scope | Independent of the caller's task scope |
| Wait for completion | await task | stopped(actor) for terminal cleanup |
| Ordinary call | Function call returns its value | Handler call returns a completion Result |
| Failure owner | Owning structured lifetime | Actor and its supervisor |
Forking an actor call makes the request operation a child task; it does not make the destination actor a child. Once the actor claims the request, abandoning the reply does not undo the handler's effects. Manage the actor itself with its lifecycle operations and supervisor.
Advanced: values, faults, and cancellation are different outcomes
An application error such as Result.Err is an ordinary task result. A task fault or scope cancellation follows the structured failure path. Keeping these separate lets a batch collect several application errors without turning the first one into automatic cancellation, while still guaranteeing cleanup for faults and deadlines.
A task handle is affine and cannot be sent as an actor message. Its result belongs to the structured lifetime that created it and can be joined once. If direct access to an unbound fork's result is discarded, the parent still drains the child; the work is not detached.
Continue from here
- Use the actor guide for tasks started around actor calls.
- Read Actor Lifecycle and Supervision for long-lived components.
- Check exact task, select, and cancellation syntax in the language reference.
- Look up released task and scope contracts in the generated concurrency reference.