Skip to main content
The Task API includes a suite of resilience operators that you compose directly onto any Task. Because Tasks are lazy, these operators describe how the task should behave when executed — they don’t trigger execution themselves.

.retry(count, options?)

Retries a failed task up to count times. Optionally applies a delay between attempts with exponential backoff.
With factor: 2 and delay: 1000, the delays between retries are: 1000 ms, 2000 ms, 4000 ms.

.timeout(ms, onTimeout)

Fails with the value returned by onTimeout if the task has not completed within ms milliseconds.

.circuitBreaker({ threshold, resetTimeout })

Protects downstream services by opening the circuit after threshold consecutive failures. While the circuit is open, the Task fails immediately with the string 'CIRCUIT_OPEN' without attempting execution. After resetTimeout milliseconds, the circuit enters a half-open state and allows one attempt through. If it succeeds, the circuit closes. If it fails, it reopens.
The circuit breaker state is held on the Task instance. Create one shared instance and reuse it across concurrent calls for the breaker to function correctly.

.throttle(concurrency)

Limits the number of simultaneous executions of the Task. Excess calls queue and wait for a slot to open. This is useful when calling a rate-limited API from code that launches tasks in parallel:
Like .circuitBreaker(), the throttle state is held on the Task instance. Reuse the same instance for the limit to take effect across concurrent callers.

.delay(ms)

Adds a fixed delay before the Task begins executing.

.withSignal(signal)

Binds an AbortSignal to the Task. If the signal is already aborted when .execute() is called, or fires while the Task is running, the Task fails immediately with the abort reason.
You can also pass a signal directly to .execute() as a shorthand:

Combining resilience operators

Resilience operators compose naturally. A realistic pattern for a production API call:
The operators apply in the order they appear:
  1. The fetch is retried up to 3 times with backoff on failure.
  2. The entire retry sequence must complete within 10 seconds.
  3. If 5 consecutive attempts fail, the circuit opens and subsequent calls return immediately.
  4. If the circuit is open (or all retries are exhausted), the fallback Task runs instead.

Caching operators

.once()

Executes the Task exactly once and caches the Result for the lifetime of the instance. Every subsequent call to .execute() returns the cached result without re-running the underlying logic. Best suited for one-time initialization work:

.memoize({ ttl?, cacheErr? })

In-memory memoization with an optional time-to-live. While the cache is valid, .execute() returns the stored Result without re-running the Task.
Setting cacheErr: true caches error results too, which prevents hammering a failing service:

.cache(key, store, { ttl? })

Delegates caching to an external store via the TaskCache interface. Useful for sharing cached data across process restarts or multiple instances.
A plain Map satisfies TaskCache:
For production use, implement TaskCache against Redis, IndexedDB, or any other store: