A Task<T, E> is a lazy async operation that doesn’t start until you call .execute(). Under the hood it always resolves to a ResultAsync<T, E>, giving you full access to the Result API after execution.
The key difference from a Promise: a Task is a description of work, not the work itself. You can attach retries, timeouts, and circuit breakers before a single network call has been made.
Creating tasks
Task.from(fn, onError)
Wraps a function that returns a Promise. The optional onError mapper converts rejected values into typed errors:
Task.ask<Context>()
Creates a task that returns the context provided via .provide(). Use this for dependency injection:
Chaining
.chain(fn)
Chains a function that returns another Task. Errors short-circuit automatically:
.map(fn) and .mapErr(fn)
Transform the value or error without creating a new async operation:
Resilience
Chain resilience operators before calling .execute(). They compose cleanly and are applied in order:
.retry(count, options)
Retries the task up to count times on failure. Pass delay (ms) and factor for exponential backoff:
.timeout(ms, onTimeout)
Fails with a custom error if the task takes longer than ms milliseconds:
.circuitBreaker({ threshold, resetTimeout })
Opens the circuit after threshold consecutive failures and rejects all calls until resetTimeout ms have elapsed:
.throttle(concurrency)
Limits the number of concurrent executions of this task:
.fallback(otherTask)
Runs otherTask if the primary task fails:
Recovery
.orElse(fn)
Recover from any error by returning a new Task:
.catchTag(errorFactory, handler)
Catch a specific tagged error variant. It is removed from the error union type after handling:
Side effects
Execution control
.delay(ms)
Waits ms milliseconds before executing:
.withSignal(abortSignal)
Cancels the task if the signal is aborted:
.once()
Executes only once and caches the result for all subsequent calls:
.memoize({ ttl? })
Like .once(), but supports a time-to-live in milliseconds after which the cache expires:
.cache(key, store)
Uses a custom cache store implementing the TaskCache interface. Useful for Redis, IndexedDB, or any external store:
Context/DI with Task.ask()
Task.ask() combined with .provide() is a lightweight dependency injection pattern that keeps your tasks testable and decoupled:
Resource management
Task.using() acquires a resource, runs a task, and guarantees the resource is released, even if the task fails:
Parallel execution
Do-notation with Task.go()
Use Task.go() to write sequential task pipelines that look imperative, without nesting .chain() calls:
Task.go() is the Task equivalent of chas.go() for Results. Both use JavaScript generators and the yield* operator as a typed short-circuit mechanism.