Proposal: Timeout and Spawn Patterns
Status: Approved Author: Eric (with AI assistance) Created: 2026-01-30 Approved: 2026-01-31 Affects: Compiler, patterns, concurrency
Summary
This proposal formalizes the timeout and spawn pattern semantics, including cancellation behavior, error handling, and relationship to other concurrency patterns.
Problem Statement
The spec shows timeout and spawn patterns but leaves unclear:
- Timeout cancellation: How is the operation cancelled?
- Timeout error: What error type is returned?
- Spawn fire-and-forget: What happens to errors?
- Spawn limits: Can spawn exhaust resources?
- Relationship: How do these relate to
parallelandnursery?
Timeout Pattern
Syntax
timeout(
op: expression,
after: Duration,
)
Semantics
Basic Behavior
- Start executing
op - If
opcompletes beforeafter: returnOk(result) - If
afterelapses first: cancelop, returnErr(CancellationError { reason: Timeout, ... })
let result = timeout(op: fetch(url), after: 5s)
// result: Result<Response, CancellationError>
Return Type
timeout(op: T, after: Duration) -> Result<T, CancellationError>
Where T is the type of op. The CancellationError has reason: Timeout.
Error Type
timeout uses the existing CancellationError type for consistency with other concurrency patterns:
type CancellationError = {
reason: CancellationReason,
task_id: int,
}
type CancellationReason =
| Timeout
| SiblingFailed
| NurseryExited
| ExplicitCancel
| ResourceExhausted
For timeout, the error is always CancellationError { reason: Timeout, task_id: 0 }.
Cancellation
Cooperative Cancellation
When timeout expires:
- Operation is marked for cancellation
- At next cancellation checkpoint, operation terminates
- Destructors run during unwinding
Err(CancellationError { reason: Timeout, task_id: 0 })is returned
Cancellation Checkpoints
Same as nursery cancellation:
- Suspending calls (functions with
uses Suspend) - Loop iterations
- Pattern entry (
run,try,match, etc.)
Uncancellable Operations
CPU-bound operations without checkpoints cannot be cancelled until they reach one:
timeout(
op: tight_cpu_loop {}, // No checkpoints inside
after: 1s,
)
// May take longer than 1s if no checkpoints
Suspend Requirement
timeout requires suspending context:
@fetch_with_timeout (url: str) -> Result<Data, Error> uses Suspend =
timeout(op: fetch(url), after: 10s)
.map_err(transform: e -> Error { message: e.reason.to_str() })
Nested Timeout
Inner timeouts can be shorter than outer:
timeout(
op: {
let a = timeout(op: step1(), after: 2s)?
let b = timeout(op: step2(), after: 2s)?
(a, b)
},
after: 5s, // Overall timeout
)
Spawn Pattern
Syntax
spawn(
tasks: [() -> T uses Suspend],
max_concurrent: Option<int> = None,
)
Semantics
Fire and Forget
spawn starts tasks and returns immediately:
spawn(tasks: [send_email(u) for u in users])
// Returns void immediately
// Emails sent in background
Return Type
spawn(tasks: [() -> T uses Suspend]) -> void
Always returns void. Results are discarded.
Error Handling
Errors in spawned tasks are silently discarded:
spawn(tasks: [
() -> {
let result = risky_operation(), // Might fail
log(msg: "done")
},
])
// If risky_operation() fails, error is silently dropped
Logging Errors
To handle errors, log explicitly within the task:
spawn(tasks: [
() -> match risky_operation() {
Ok(_) -> log(msg: "success")
Err(e) -> log(msg: `failed: {e}`)
},
])
Concurrency Control
max_concurrent
Limit simultaneous tasks:
spawn(
tasks: [send_email(u) for u in users],
max_concurrent: Some(10),
)
// At most 10 emails sending at once
Default Behavior
When max_concurrent is None (default), all tasks may start simultaneously.
Resource Exhaustion
If runtime cannot allocate resources:
- Task is dropped
- No error surfaced (fire-and-forget semantics)
- Other tasks continue
No Wait Mechanism
spawn provides no way to wait for completion:
spawn(tasks: tasks)
// Cannot wait here
For waiting, use parallel or nursery:
let results = parallel(tasks: tasks)
// Wait for all to complete
Task Lifetime
Spawned tasks:
- Run independently of the spawning scope
- May outlive the spawning function (true fire-and-forget)
- Complete naturally, are cancelled on program exit, or terminate on panic
Note:
spawnis the ONLY concurrency pattern that allows tasks to escape their spawning scope. Unlikeparallelandnursery, which guarantee all tasks complete before the pattern returns,spawntasks are managed by the runtime and may continue after the spawning function returns. For structured concurrency with guaranteed completion, usenursery.
@setup () -> void uses Suspend = {
spawn(tasks: [background_monitor()])
// Function returns, but monitor continues
}
Comparison with Other Patterns
| Pattern | Returns | Waits | Errors | Scoped | Use Case |
|---|---|---|---|---|---|
timeout | Result<T, CancellationError> | Yes | Surfaced | Yes | Bounded wait |
spawn | void | No | Dropped | No | Fire-and-forget |
parallel | [Result<T, E>] | Yes | Collected | Yes | Batch operations |
nursery | [Result<T, E>] | Yes | Configurable | Yes | Structured concurrency |
Examples
Timeout with Fallback
@fetch_with_fallback (url: str, fallback: Data) -> Data uses Suspend =
match timeout(op: fetch(url), after: 5s) {
Ok(data) -> data
Err(_) -> fallback
}
Spawn Background Tasks
@on_user_signup (user: User) -> void uses Suspend = {
save_user(user), // Synchronous, must complete
spawn(tasks: [
() -> send_welcome_email(user)
() -> notify_admin(user)
() -> update_analytics(user)
]), // Fire and forget
}
Timeout in Loop
@fetch_all (urls: [str]) -> [Option<Data>] uses Suspend =
for url in urls yield
match timeout(op: fetch(url), after: 5s) {
Ok(data) -> Some(data)
Err(_) -> None
}
Spawn with Rate Limiting
@notify_all_users (users: [User]) -> void uses Suspend =
spawn(
tasks: [() -> send_notification(u) for u in users],
max_concurrent: Some(50), // Avoid overwhelming notification service
)
Error Messages
Timeout Missing Suspend
error[E1010]: `timeout` requires `Suspend` capability
--> src/main.ori:5:5
|
5 | timeout(op: fetch(url), after: 5s)
| ^^^^^^^ requires `uses Suspend`
|
= help: add `uses Suspend` to the function signature
Spawn Task Not Suspending
error[E1011]: `spawn` tasks must use `Suspend`
--> src/main.ori:5:18
|
5 | spawn(tasks: [() -> sync_function()])
| ^^^^^^^^^^^^^^^^^^^^^^^ missing `uses Suspend`
|
= note: spawn requires suspending tasks for concurrent execution
Spec Changes Required
Update 10-patterns.md
Add comprehensive sections for:
- Timeout semantics and cancellation
- Spawn fire-and-forget behavior
- Comparison table with other patterns
- Clarify spawn is only unscoped concurrency pattern
Summary
Timeout
| Aspect | Details |
|---|---|
| Syntax | timeout(op:, after:) |
| Returns | Result<T, CancellationError> |
| Cancellation | Cooperative at checkpoints |
| Requires | uses Suspend |
| Use case | Bounded waiting for operations |
Spawn
| Aspect | Details |
|---|---|
| Syntax | spawn(tasks:, max_concurrent:) |
| Returns | void |
| Errors | Silently dropped |
| Requires | uses Suspend |
| Scoped | No (tasks may outlive spawner) |
| Use case | Fire-and-forget background work |