Task — Lazy Asynchronous Work
4.55 kB minified + gzipped
A build service prepares two requests: one for the location of a finished artifact and one for the artifact itself. When both start while the chain is still being assembled, a retried render downloads the artifact twice and the two results can disagree. When nothing starts the chain, the screen stays empty and no error explains why.
Core Behavior
Section titled “Core Behavior”Task<A> describes asynchronous work as a function that takes an optional AbortSignal and returns
an awaitable value; creating or combining tasks starts nothing.
- Calling the task runs the work. Calling it again runs the work again unless
Task.memoizehas captured the first result. Task.Result<E, A>carries failure in the type,Task.Maybe<A>carries absence, andTask.Validation<E, A>collects errors from independent checks.- The signal passed at the call site reaches the work. Work that ignores the signal keeps running after the caller has stopped waiting.
- A base task assumes its body does not throw or reject.
Task.Result.tryCatchis the boundary where a throw becomes a typed error.
Reach for Task when one lazy piece of work has to be described before it runs. When calls can
overlap and a policy must decide which one wins, Op wraps the same task in a
manager. Reading the Types separates definitions from running instances, and the
choosing guide maps lazy work to the modules that run it.
Example: Build Artifact
Section titled “Example: Build Artifact”The build service reads an artifact location, then reads the artifact from that location. The second request needs the value from the first, so it must not start when the first fails.
Task.Result.chain passes an Ok value to the next request. An existing Err skips the next
request and leaves the error untouched:
flowchart TD
Start["Call the loader"] --> Read["Read the artifact location"]
Read --> Outcome{"Result"}
Outcome -->|Err| Failed["Return that error"]
Outcome -->|Ok| Next["Read the artifact from the location"]
Next --> Done["Return the artifact result"]
Loading a Build Artifact
Section titled “Loading a Build Artifact”The loader maps each request failure to a string, so the pipeline has one error type. Task.run()
is the line that starts the requests:
loadArtifact("build-42") returns a description of two requests. The chain inside it runs only when
Task.run() starts the pipeline, and a second Task.run() would send both requests again.
Running the Same Loader Under a Policy
Section titled “Running the Same Loader Under a Policy”A build page can request several builds as the user moves between rows. Op.make takes the loader
factory unchanged, and the manager decides what a second call does to the first:
With restartable, the earlier caller settles as OpNil("replaced") while the newer call owns the
manager state. The Op guide covers the other strategies and the retry and timeout
options.
Core Operations
Section titled “Core Operations”Creating Tasks
Section titled “Creating Tasks”| Signature | What it does | Return type |
|---|---|---|
Task.make(value) |
Returns the value on every call. | Task<A> |
Task.from.sync(fn) |
Runs a synchronous function on every call. | Task<A> |
Task.tryCatch(fn, { onError }) |
Runs promise-returning work and maps a throw or rejection to a fallback value. | Task of the value or the fallback |
Task.Result.tryCatch(fn, { onError }) |
Maps a throw or rejection to a typed error. | Task.Result<E, A> |
Task.Maybe.tryCatch(fn) |
Maps a throw or rejection to None. |
Task.Maybe<A> |
Task.Validation.tryCatch(fn, { onError }) |
Maps a throw or rejection to a failed validation. | Task.Validation<E, A> |
Wrapping Existing Values
Section titled “Wrapping Existing Values”| Signature | What it does | Return type |
|---|---|---|
Task.Result.make.ok(value) / make.err(error) |
Wraps a success or an error. | Task.Result<E, A> |
Task.Result.from.Result / from.Maybe / from.nullable / from.Task |
Lifts a container, a nullable, or an infallible task into the fallible channel. | Task.Result<E, A> |
Task.Maybe.make.some / make.none; Task.Maybe.from.* |
Wraps or lifts an optional value. | Task.Maybe<A> |
Task.Validation.make.passed / failed / failedAll; Task.Validation.from.* |
Wraps or lifts a validation. | Task.Validation<E, A> |
Transforming Values
Section titled “Transforming Values”| Signature | What it does | Behavior |
|---|---|---|
Task.map(transform) |
Transforms the resolved value. | Returns Task of the new value. |
Task.chain(next) |
Runs a task built from the resolved value. | The next task starts after the first succeeds. |
Task.Result.map / Task.Result.mapError |
Transforms the success or the error value. | The other channel passes through. |
Task.Result.chain(next) |
Runs fallible work built from an Ok value. |
An Err skips next; error types union. |
Task.tap(sideEffect); Task.Result.tap / tapError |
Observes a value or an error without replacing it. | Returns the original task. |
Task.assignTo(key) / Task.assign(key, next) |
Starts a record and adds dependent results to it. | assign runs next with the accumulator. |
Combining Tasks
Section titled “Combining Tasks”| Signature | What it does | Behavior |
|---|---|---|
Task.all(tasks, { concurrency }) |
Runs tasks in parallel and collects a tuple. | Nonpositive or non-integer concurrency clamps to 1; omitted runs unbounded. |
Task.race(tasks) |
Settles with the first task to resolve. | Aborts the other tasks’ signals. |
Task.apply(argument) |
Applies a task-wrapped function to a task-wrapped value. | Both tasks run in parallel. |
Task.Result.all(tasks, { concurrency }) |
Runs fallible tasks in parallel. | The first Err cancels active work; a caller abort settles as Aborted. |
Task.Result.struct(fields) |
Combines a record of fallible tasks. | Waits for every field; the first error in key order wins. |
Task.Maybe.struct(fields) |
Combines a record of optional tasks. | None when any field is missing. |
Task.Validation.struct(fields) |
Combines a record of validations. | Collects every error in key order. |
Task.Result.allSettled(tasks) |
Collects each outcome. | Keeps successes and errors per position. |
Running and Controlling
Section titled “Running and Controlling”| Signature | What it does | Behavior |
|---|---|---|
task(signal) / Task.run(signal) |
Runs a task directly or at the end of a pipe. | Returns Deferred<A>. |
Task.Result.run / Task.Maybe.run / Task.Validation.run |
Runs a specialized task. | Returns the deferred container. |
Task.delay(duration) |
Waits before running the task. | Aborting the wait starts the task with the aborted signal. |
Task.timeout({ duration, onTimeout }) |
Adds a deadline to an infallible task. | Returns a Task.Result; onTimeout supplies the error. |
Task.Result.timeout({ duration, onTimeout }) |
Adds a deadline to a fallible task. | Appends the timeout error to the error union. |
Task.repeat({ times, delay }) |
Runs the task a fixed number of times. | Returns Task of a result array. |
Task.poll({ until, delay, attempts }) |
Runs the task until a predicate passes. | Returns the last value at the attempt limit. |
Task.Result.poll({ until, delay, attempts }) |
Repeats fallible work until a predicate passes. | The first Err ends the loop. |
Task.Result.paginate(load) |
Follows a loader’s cursor until a page has no next. |
The first Err ends the run; collected pages are dropped. |
Task.memoize(task) |
Shares the first execution and its result. | Later calls reuse the same deferred value. |
Task.abortable(factory) |
Creates a task with an abort handle. | A new call aborts the previous one. |
Task.withProgress(onProgress) |
Reports 0 before the work and 1 after it settles. |
Returns a task that reports progress. |
Task.withLabel(label) |
Attaches a read-only label. | Returns a labeled task. |
Task.Result.retry(policy) |
Repeats failed work according to a RetryPolicy. |
Stops on success, a filtered error, or exhausted attempts. |
Failure Behavior and Edge Cases
Section titled “Failure Behavior and Edge Cases”Task.Result.struct starts every field and waits for all of them, even when one returns Err
early. If several fields fail, the first error in field order wins, regardless of completion order:
Task.Result.allSettled keeps every outcome, and Task.Validation.struct collects every error
instead of returning one. Task.Result.all is stricter: it cancels the tasks that are still running
and settles with the first Err, and aborting its caller settles as Aborted (see
Aborted).
Task.Result.poll repeats work that succeeded but is not ready yet, so a failed attempt ends the
loop; Task.Result.retry repeats work that failed. Compose them when both apply: the retry sits
inside the polled task and covers one attempt, while the poll decides when the value is final.
Task.race([]) never resolves. Task.memoize shares the first invocation and ignores every
caller’s signal; later callers do not start new work. For fallible, keyed lookups with expiry and
eviction, use Cache instead. Task.all forwards the call-site signal to every
task, including tasks that start later.
Task.all clamps a nonpositive or non-integer concurrency to 1. Task.repeat floors a finite
times and returns an empty array for a non-finite or nonpositive count; aborting it returns the
results collected so far. attempts is a positive integer; Task.poll and Task.Result.poll treat
anything that is not a positive integer as one attempt, and omitting it polls without a limit.
Aborting either loop stops it before the next run, while the in-flight run still finishes.
Task.Result.paginate requests pages sequentially and stops at the first Err, so the pages
already collected are dropped rather than returned beside the error. A run that must resume persists
the cursor itself, and the caller’s signal is the only stop control: a loader whose next never
clears loops until that signal aborts.
Task.delay starts the wrapped task as soon as the wait is aborted, passing the aborted signal, so
the task sees a cancelled signal immediately.
A base Task assumes its work and callbacks do not throw or reject; a throw escapes the deferred
value. Put fallible work behind Task.Result.tryCatch. Cancellation is cooperative: abortable,
race, timeout, and delay signal the underlying work, and work that ignores the signal keeps
running. abortable does not convert a rejection caused by an abort into a typed result.
Task.timeout and Task.Result.timeout call onTimeout inside a timer. A throw from that callback
surfaces as an uncaught exception and no timeout error reaches the caller, so handle expected
failures before constructing the timeout error.