APIAsync
Async
Concurrent tasks, cancellation, and channels.
run starts a root lifetime scope. Every task owns its children; returning
from a task or scope cancels unfinished children and drains their cleanup.
Await work whose normal completion matters. scope gives helpers a shorter
lifetime; both, all, and race own their tasks and handle application Fail.
Raw tasks return ordinary values, including Results: an Err does not cancel
siblings. Handle application aborts inside spawned callbacks; inherited
resumptive handlers share their state cells.
Cancellation is cooperative: sleeps, channel transfers, and task waits
respond to it, while CPU loops need checkpoint. Blocking IO is interrupted
only by cancellation-aware APIs such as Net. cancelled, cancel, and close
still work after cancellation; cancellation-sensitive operations in cleanup
stop immediately. Draining itself cannot be interrupted. A task that never
responds to cancellation can prevent its owner from finishing. Cancellation
remains the primary outcome when cleanup also fails; its suppressed failures
are discarded. See the language reference under “Handlers in tasks”.
Channel
type Channel valueSource
A channel carrying values between tasks. It lives independently of the tasks that use it.
Outcome
type Outcome value = Completed value | CancelledSource
How a task or root body ended.
Constructors
Instances:
Eq value => Eq (Outcome value)
Show value => Show (Outcome value)
Task
type Task valueSource
A running or finished task that returns a value.
A handle may outlive its owning scope, but does not extend the task’s lifetime.
Async
effect AsyncSource
Running inside a structured task scope. run, runOutcome, and scope
handle it.
all
all : List (() ->{Fail a, e} b) ->{Fail a, Async, e} List bSource
Starts every action concurrently and returns values in input order. Empty input returns []. The first observed application Fail cancels remaining tasks; after their cleanup it is raised in the caller. A cancelled participant cancels the caller instead. Caller cancellation also drains all owned work. Caller cancellation before drain finishes takes precedence over a selected Fail. Failures are observed after the failing task’s cleanup, not at the raise. Later errors during drain are discarded. Other abort effects need local handling; a returned Err is ordinary data and does not trigger this policy.
import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))
result : Outcome (Result String (List Int))
result = Async.runOutcome { Fail.attempt { Async.all [{ 1 }, { 2 }] } }
result == Completed (Ok [1, 2])
await
await : Task a ->{Async} aSource
Waits for the task’s value. A cancelled target cancels the observing task
and its descendants, including when called inside a nested scope;
use awaitOutcome to choose a different response. Returned Results remain
ordinary values; use Fail.fromResult explicitly to raise an error here.
The spawn example shows awaiting successful work; awaitOutcome shows
inspecting cancellation without propagating it.
awaitAll
awaitAll : List (Task a) ->{Async} List aSource
Waits for all task values in input order. Empty input returns []. A cancelled
target cancels the observing task and drains its descendants. Returned Err
values are ordinary data; use all to cancel other work on application Fail.
awaitAny
awaitAny : List (Task a) ->{Async} Maybe (Int, a)Source
Observes one task’s value and its zero-based input index. Empty input returns
Nothing; a selected cancellation cancels the observing task and descendants.
Selection and repeated observations follow awaitAnyOutcome.
import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))
Async.runOutcome { Async.awaitAny [Async.completed 7] } == Completed (Just (0, 7))
awaitAnyOutcome
awaitAnyOutcome : List (Task a) ->{Async} Maybe (Int, Outcome a)Source
Observes one task’s terminal outcome and its zero-based input index. Empty input returns Nothing. A selected cancellation is data. Simultaneously ready tasks may be selected in any order; repeated calls may select the same task. Waiter cancellation interrupts selection without adopting or cancelling its targets, except through an existing ownership relationship.
import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))
result = Async.runOutcome {
task = Async.completed 42
Async.awaitAnyOutcome [task]
}
result == Completed (Just (0, Completed 42))
awaitOutcome
awaitOutcome : Task a ->{Async} Outcome aSource
Waits for completion and returns the value or the child’s cancellation. Repeated and concurrent observations see one stable outcome. Cancellation of the observer interrupts its wait. Waiting creates no ownership or cancellation relationship; an owned descendant still follows tree cancellation. A target’s Cancelled outcome does not cancel the observer.
import Async exposing (Outcome(..))
result = Async.runOutcome {
gate : Async.Channel Int
gate = Async.channel 0
task = Async.spawn { Async.receive gate }
Async.cancel task
Async.awaitOutcome task
}
result == Completed Cancelled
both
both : (() ->{Fail a, e} b) -> (() ->{Fail a, e} c) ->{Fail a, Async, e} (b, c)Source
Starts two actions concurrently and returns their values as a pair. Each may
return a different type, but they report the same Fail error type. Cancellation,
first-observed failure, draining, and returned Err data follow all.
import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))
result : Outcome (Result String (Int, String))
result = Async.runOutcome { Fail.attempt { Async.both { 1 } { "two" } } }
result == Completed (Ok (1, "two"))
cancel
cancel : Task a ->{Async} ()Source
Requests the task’s cancellation, and its children’s. It does not cancel the caller or siblings. Returns without waiting for cleanup; awaitOutcome waits for that cleanup and inspects cancellation, as in its example.
cancelled
cancelled : () ->{Async} BoolSource
Whether the current task’s active lifetime scope has been cancelled. It remains usable during cancellation cleanup; it does not stop the task.
import Async exposing (Outcome(..))
Async.runOutcome { Async.cancelled() } == Completed False
channel
channel : Int ->{Async} Channel aSource
A new channel buffering up to capacity values. Zero or less makes it
unbuffered: each send waits for a receiver. Channels are independent of task
ownership and may outlive their creating scope.
import Async exposing (Outcome(..))
import Maybe exposing (Maybe(..))
result = Async.runOutcome {
messages = Async.channel 1
ignore (Async.send messages 42)
Async.close messages
first = Async.receive messages
next = Async.receive messages
rejected = Async.send messages 7
(first, next, rejected)
}
result == Completed (Just 42, Nothing, False)
checkpoint
checkpoint : () ->{Async} ()Source
Leaves the current task if it has been cancelled, running cleanup on the way; otherwise does nothing. Long computations call it to respond to cancellation.
close
close : Channel a ->{Async} ()Source
Closes the channel, waking blocked senders and receivers. Closing it again
does nothing.
It remains usable after task cancellation. Buffered values can still be
received before Nothing; see the channel example.
completed
completed : a ->{Async} Task aSource
A task already completed with value. An Err is ordinary data.
import Async exposing (Outcome(..))
Async.runOutcome { Async.await (Async.completed 7) } == Completed 7
contextToken
contextToken : () ->{Async} AnySource
The current lifetime’s opaque cancellation context, for native sidecars that must interrupt a blocking operation when the task or its scope ends. It remains accessible during cancellation cleanup; obtaining it is not a checkpoint.
parMap
parMap : (a -> b) -> List a -> List bSource
Applies a pure function to every element in parallel, keeping their order.
It uses at most GOMAXPROCS workers per call and needs no task scope. An
empty list never calls the function.
import Async
Async.parMap { n -> n * n } [1, 2, 3] == [1, 4, 9]
race
race : List (() ->{Fail a, e} b) ->{Fail a, Async, e} bSource
Starts every action concurrently and selects the first observed terminal outcome. A winning value is returned, a winning Fail is raised in the caller, and a winning cancellation cancels the caller. Empty input cancels the caller. Remaining tasks are cancelled and drained before return or propagation, so this is a prompt cancellation request, not a bounded return deadline. Simultaneous outcomes may win in any order. Later failures during drain are discarded. A returned Err is an ordinary winning value. Handle other abort effects inside the actions. This does not search for the first success. Caller cancellation before drain finishes takes precedence over the winner.
import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))
result : Outcome (Result String Int)
result = Async.runOutcome {
Fail.attempt {
Async.race [
{ 42 }
, {
gate : Async.Channel Int
gate = Async.channel 0
ignore (Async.receive gate)
Fail.fail "timeout"
}
]
}
}
result == Completed (Ok 42)
receive
receive : Channel a ->{Async} Maybe aSource
The next value, waiting while none is buffered, or Nothing once the
channel is closed and drained.
Cancellation stops the receiving task instead of returning Nothing. See
channel for a buffered send/close/drain example.
run
run : (() ->{Async, e} ()) ->{IO, e} ()Source
Runs a Unit-returning body in a new root scope. On every exit it cancels
unfinished children, including ignored handles, and drains their cleanup.
Returns Unit after completion or cancellation,
so use Async.run fits a Unit-returning block such as main().
Application aborts leaving the body cancel and drain its children before
propagating. The caller must permit the body’s residual effects; run
introduces IO. Use runOutcome to inspect completion or cancellation.
import Async
Async.run { ignore (Async.await (Async.spawn { 7 })) } == ()
runOutcome
runOutcome : (() ->{Async, e} a) ->{IO, e} Outcome aSource
Runs body in a new root scope, returning its value or cancellation after
cancelling and draining unfinished children, including ignored handles.
Application aborts leaving the body drain its children before their outer handler.
The caller must permit the body’s residual effects; runOutcome introduces IO.
import Async exposing (Outcome(..))
Async.runOutcome { Async.await (Async.spawn { 42 }) } == Completed 42
scope
scope : (() ->{Async, e} a) ->{Async, e} aSource
Runs body on the calling task with a shorter lifetime for its children.
Every exit cancels unfinished children and drains their cleanup. Normal exit
returns the body’s value without cancelling the caller or outside tasks.
Cancellation propagates through the calling task; it is not a scope result.
Keep this scope inside any resource scope its tasks use. Unfinished tasks
cannot escape by returning their handles; the handles keep their final outcomes.
import Async exposing (Outcome(..))
result = Async.runOutcome {
Async.scope {
gate : Async.Channel Int
gate = Async.channel 0
ignore (Async.spawn { Async.receive gate })
42
}
}
result == Completed 42
send
send : Channel a -> a ->{Async} BoolSource
Sends a value, waiting while the buffer is full. Answers False when the
channel is closed, including while waiting.
Cancellation stops the sending task instead of returning False. See
channel for a buffered send/close/drain example.
sleep
sleep : Int ->{Async} ()Source
Pauses the task for milliseconds, waking early when it is cancelled. A
duration of zero or less is a checkpoint.
import Async exposing (Outcome(..))
Async.runOutcome { Async.checkpoint(); Async.sleep 0; 42 } == Completed 42
spawn
spawn : (() ->{e} a) ->{Async, e} Task aSource
Starts body as a child of the current lifetime scope and returns its handle
at once. The child owns its own spawns: when its body exits, unfinished
descendants are cancelled and drained before its outcome is published.
Await children that must finish normally. There is no detached spawn.
The caller
must permit all of the body’s effects, including IO, even for ignored
handles. Application aborts must be handled inside the callback; known
escapes are rejected statically and hidden dependencies before user code
runs. An ordinary returned Err is a value and does not cancel siblings.
import Async exposing (Outcome(..))
result = Async.runOutcome {
first = Async.spawn { 10 }
second = Async.spawn { 20 }
Async.awaitAll [first, second]
}
result == Completed [10, 20]
spawnAttempt
spawnAttempt : (() ->{Fail a, e} b) ->{Async, e} Task (Result a b)Source
Starts action with its named Fail application handled inside the child,
returning a task whose value is a Result. Other abort dependencies still
need local handling. An Err does not cancel siblings or fail the scope.
import Async exposing (Outcome(..))
import Fail
import Result exposing (Result(..))
Async.runOutcome { Async.await (Async.spawnAttempt { Fail.fail "missing"; 1 }) } == Completed (Err "missing")
wait
wait : Task a ->{IO} Outcome aSource
Waits, outside any task scope, for the task to finish and answers how it ended. This IO wait does not provide an Async cancellation point. Repeated observations keep the same outcome, including after its owner has exited.
import Async exposing (Outcome(..))
saved = Async.runOutcome { Async.completed 7 }
result = case saved of
Completed task -> Async.wait task
Cancelled -> Cancelled
result == Completed 7
