Task — async operations
Task<A> is an async computation with two guarantees: it is lazy (nothing runs until you call it) and infallible (it always resolves — it never rejects). When failure is possible, that failure is encoded in the return type using TaskResult<E, A> rather than leaking out as a rejected Promise.
The problems with Promises
Section titled “The problems with Promises”Promises have two quirks that make them hard to compose.
Promises are eager. A Promise starts the moment it’s created:
You can’t build a pipeline of async steps and pass it around before any work begins — by the time you have the Promise in hand, the work is already underway.
Promises can reject. Failure leaks out as an untyped exception rather than as a typed value. This forces try/catch at every call site and makes it impossible to tell from a function’s return type whether it can fail.
The Task approach
Section titled “The Task approach”A Task<A> is just a zero-argument function returning a Promise<A>:
This addresses both problems. The function wrapper makes it lazy — nothing runs until you call it. And by treating Tasks as always-succeeding computations, failure is pushed into the type: TaskResult<E, A> is Task<Result<E, A>>, so it’s impossible to overlook.
The pipeline is built first, then executed once by calling it. You can pass it around, compose it further, or call it multiple times.
Creating Tasks
Section titled “Creating Tasks”Task.from is an explicit alias for writing () => somePromise(). It’s mainly useful for clarity:
Transforming with map
Section titled “Transforming with map”map transforms the resolved value without running the Task:
Chaining maps builds a description of the transformation; the actual async work happens when you call the result.
Sequencing with chain
Section titled “Sequencing with chain”chain sequences two async operations where the second depends on the result of the first:
Each step waits for the previous one to resolve before starting.
Running Tasks in parallel with all
Section titled “Running Tasks in parallel with all”Task.all takes an array of Tasks and runs them simultaneously, collecting all results:
The return type is inferred from the input tuple — if you pass [Task<Config>, Task<string>], you get back Task<[Config, string]>.
Delaying execution
Section titled “Delaying execution”Task.delay adds a pause before the Task runs:
Useful for debouncing or rate limiting.
Repeating Tasks
Section titled “Repeating Tasks”Unlike retry — which re-runs a computation in response to failure — repeat and repeatUntil run a Task multiple times unconditionally. This fits naturally with Task’s guarantee that it never fails.
Task.repeat runs a Task a fixed number of times and collects every result:
Task.repeatUntil keeps running until the result satisfies a predicate, then returns it. This is the natural shape for polling:
Both accept an optional delay (in ms) inserted between runs. The delay is not applied after the final run.
The Task family
Section titled “The Task family”Task<A> is for async operations that always succeed. When failure is possible, use the specialised variants:
TaskResult<E, A> — an async operation that can fail with a typed error. It’s Task<Result<E, A>> under the hood:
TaskOption<A> — an async operation that may return nothing. It’s Task<Option<A>>:
TaskOption.tryCatch catches any rejection and converts it to None — useful when you treat a failed lookup the same as a missing value.
TaskValidation<E, A> — an async operation that accumulates errors. Used for async validation where all checks should run regardless of individual failures.
All three follow the same API conventions as their synchronous counterparts (map, chain, match, getOrElse, recover). If you’ve used Result, TaskResult will be immediately familiar.
Running a Task
Section titled “Running a Task”A Task is just a function. To run it, call it:
For TaskResult and TaskOption, the result is a wrapped value:
Most of the time you’ll call the pipeline at one point — the outer boundary where your application produces a final result or triggers a side effect.
When to use Task vs async/await
Section titled “When to use Task vs async/await”Use Task when:
- You want to build a pipeline of async steps that you can compose, pass around, or delay before executing
- You need parallel execution via
Task.allwithin a pipeline - You want typed error handling with
TaskResultinstead of try/catch around async functions
Keep using async/await directly when:
- The operation is a one-liner with no composition needed
- You’re inside a function body and the imperative style is clearer
- You’re working with code that isn’t pipeline-oriented
The two styles interoperate freely. Task.from(() => someAsyncFunction()) wraps any async function into a Task, and calling a Task gives back a plain Promise that async/await handles normally.