race is a value expression. It starts every arm as a task, waits for one arm
to complete, cancels the other tasks, and yields the winner’s value.
response := race {
fetch(primary)
fetch(replica)
}Commas between arms are optional; one expression per line is clearest.
All arm bodies are started before the wait begins. Side effects within those bodies can therefore overlap and must be safe even for an arm that later loses.
Result types
Compatible arm types unify into the race result:
fn fastest(): int {
return race {
calculate_locally()
calculate_remotely()
}
}Heterogeneous arms may widen to an anonymous union under the same branch unification rules as other expressions. Prefer a single explicit result type for public code so callers do not need to match implementation-dependent arm types.
winner: Response = race {
fetch(primary)
fetch(replica)
}An expected type from an annotation or return contract is checked against every arm.
Completion
The current compiler lowers a race to:
- spawn every arm;
- select the first completed task;
- cancel every losing task;
- await and return the winner.
This is a completion race. Do not rely on older prose that promised the first successful arm while silently ignoring earlier failures. A fallible winning arm follows the normal task and error-propagation behavior.
result := race {
fetch(primary)
fetch(replica)
}
response := result?If the first completed arm is an error, that error wins even if another arm would later succeed. Implement “first success” as an explicit protocol that collects failures and continues waiting.
Cancellation
Losers are canceled automatically. Use select instead when non-winning tasks
must continue or when each source needs a different statement body.
Cancellation is cooperative, so a losing task stops at a scheduler or suspension safepoint rather than at an arbitrary instruction.
Loser cleanup still runs for live owned values and resource wrappers. Avoid irreversible side effects before the point where an arm is safe to abandon, or design those operations to be idempotent.
Empty races
race {} is accepted as a degenerate void expression with no runtime work.
The current checker records the race effect row before recognizing the empty
case, so its static row still includes Spawn, Suspend, and Cancel. Require
at least one arm in normal code.
Effects
A non-empty race contributes:
Spawn, because each arm becomes a child task;Suspend, because the caller waits for a winner;Cancel, because losers are stopped.
Choosing a construct
Use race when all arms implement interchangeable ways to obtain one value.
Use select when arms have distinct meanings, types, or side effects. Use
ordinary spawn plus ordered .await() calls when every result is required.
Ordering
When multiple arms are already complete at the readiness scan, an earlier handle can win. Race is not a randomness primitive and does not promise fair distribution between equally fast arms.
The result identifies only the value, not which source won. Wrap values in a small enum or record when source identity is part of the caller’s decision.