| id | stdlib-task |
|---|---|
| title | Task Module |
| slug | /stdlib/task |
The Task module creates and composes concurrent work. All functions are optional .NET runtime externs: the CLI provides them by default; embedding hosts can register TaskExterns.all or a subset. --no-default-externs removes them. Canonical task execution is unsupported in Fable/JavaScript and in the standalone LIR interpreter; both compiled .NET backends implement the API.
A task has the native postfix type 'a task. Lists are homogeneous: every candidate in a task list must return the same type. Callbacks return ordinary values, and their return value becomes the new task's result. No operation automatically flattens a task returned by a callback.
| Function | Signature |
|---|---|
Task.spawn |
(unit -> 'a) -> 'a task |
Task.delay |
int -> unit task |
Task.fromResult |
'a -> 'a task |
Task.continueWith |
('a -> 'b) -> 'a task -> 'b task |
Task.whenAll |
('a list -> 'b) -> 'a task list -> 'b task |
Task.whenAny |
('a -> 'b) -> 'a task list -> 'b task |
Task.await |
'a task -> 'a |
Task.awaitAll |
'a task list -> 'a list |
Schedules a unit -> 'a function concurrently and returns a task immediately.
let pending = Task.spawn (fun () -> 40 + 2)
Task.await pending
// 42Creates a task that completes with () after the specified milliseconds. It uses a timer, without occupying a worker thread during the delay. Allowed values are 0..2147483647; negative or larger values raise an error. Zero produces a completed task. There is no infinite-delay or script cancellation overload. Stopping a debugger session cancels its outstanding timers so a long delay does not hold the session open.
let pending = Task.delay 100
Task.await pending
// ()Creates an already-completed task containing the supplied value. The value is evaluated before the call. The task still needs to be awaited, directly or through a consuming combinator.
Task.fromResult "ready" |> Task.await
// "ready"Takes the callback first and the task last, so it composes naturally with pipelines. The callback is scheduled asynchronously after successful completion of the input and receives its result directly. Its value becomes the returned task's result. Prerequisite failure skips the callback and propagates failure.
let next =
Task.fromResult 41
|> Task.continueWith (fun value -> value + 1)
Task.await next
// 42Unlike .NET's task-handle-based ContinueWith, this callback receives the completed value and runs only on success. It has no scheduler, status-filter, or cancellation overloads. A separate Task.map would duplicate this operation and is not provided.
Takes the callback first and a list of already-created tasks last. It waits asynchronously for every input, then schedules the callback once with results in input order, independently of completion order. The function does not start the input work again. Input failure skips the callback. An empty input invokes the callback with [].
let tasks = [Task.fromResult 20; Task.fromResult 22]
let total =
tasks
|> Task.whenAll (fun values ->
List.fold (fun sum value -> sum + value) 0 values)
Task.await total
// 42List.init 0 Task.fromResult
|> Task.whenAll List.length
|> Task.await
// 0Takes the callback first and a nonempty list of already-created tasks last. The first observed completion selects a winner; the callback receives the winning value, rather than a task handle. It runs once after successful completion of that winner. A failed winner propagates failure without calling the callback. This selects the first completion, not the first success. Empty input raises an error because there is no value to supply.
If multiple inputs are already complete when the race is registered, the first completed input in list order is selected. Otherwise selection depends on scheduling. Other candidates keep running and are not cancelled. Await them explicitly to use their results; otherwise the runtime joins them before execution returns. Their failures still fail the whole execution.
let timers = [Task.delay 10; Task.delay 20]
let winner = timers |> Task.whenAny (fun () -> "a timer completed")
let message = Task.await winner
Task.awaitAll timers |> ignore
messageA list of unit task is valid: its callback can be fun () -> .... For tasks producing different types, map their results into a user-declared discriminated union using continueWith:
type Completion =
| Downloaded of string
| TimedOut
let download = Task.fromResult "content"
let candidates =
[ download |> Task.continueWith Downloaded
Task.delay 100 |> Task.continueWith (fun () -> TimedOut) ]
let handled =
candidates
|> Task.whenAny (fun result ->
match result with
| Downloaded text -> $"received:{text}"
| TimedOut -> "timed out")
let message = Task.await handled
// Observe the remaining work before the program finishes.
Task.awaitAll candidates |> ignore
messageNo synthetic union or special matcher is needed. The usual type checker verifies a common Completion result type, and the callback uses ordinary pattern matching. This example does not cancel the download or abandon work on timeout.
Waits for one task and returns its result. Waiting synchronizes script execution; inside evaluator workers and the debugger, it suspends the continuation instead of occupying a scheduler worker. An input task can be awaited more than once.
let pending = Task.spawn (fun () -> 42)
Task.await pending
// 42Waits for all input tasks and returns their results in input order. It returns a list, matching the homogeneous list input; it does not construct a runtime-sized tuple. Empty input returns [].
Task.awaitAll [Task.fromResult "first"; Task.fromResult "second"]
// ["first";"second"]A callback returning 'b task produces ('b task) task. Await both layers explicitly:
let outer =
Task.fromResult 41
|> Task.continueWith (fun value ->
Task.fromResult (value + 1))
let inner = Task.await outer
Task.await inner
// 42The same rule applies to spawn, fromResult, whenAll, and whenAny.
- The runtime joins every task it starts before returning to the host, including unobserved tasks, losing
whenAnycandidates, and tasks started by other tasks. Explicit awaits are needed to use results, not to prevent detached background work. - Background failures do not stop independent sibling tasks. After all tasks finish, the runtime reports all failures together, including a foreground failure if present. A failed prerequisite prevents its dependent callback from running. Propagating the same failure through several tasks does not duplicate it in the report.
- The evaluator reports collected failures as an
EvalException; generated .NET code usesAggregateExceptionfor multiple failures. A single failure retains its ordinary exception form. - This applies to top-level evaluator execution, evaluator calls to exported functions, and generated program
Runboundaries. Native exported functions invoked separately by a .NET host retain host-owned CLR task lifetimes. - Nested task results stay nested. Await each layer to retrieve its value; the runtime also joins any nested task left unobserved.
- Returning or throwing in foreground code does not detach outstanding tasks. A task or host extern that never completes prevents execution from returning; hosts still need resource and timeout limits.
- Callbacks run concurrently and may interleave effects. Their captured FFI state must be safe to share; use the task-sharing analysis to check potential races.
- These are runtime scheduling capabilities, not pure core functions. Delay observes time and
whenAnyobserves completion order. They expose no filesystem or network IO, and hosts may omit the entire module for deterministic sandboxed execution.