Skip to main content
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.
import { Task } from 'ts-chas/task';

// Define the work — nothing runs yet
const fetchTask = Task.from(
  () => fetch('/api/data').then((r) => r.json()),
  (e) => new Error(`Network error: ${e}`)
);

// Execute when ready
const result = await fetchTask.execute(); // Result<unknown, Error>

Creating tasks

Task.from(fn, onError)

Wraps a function that returns a Promise. The optional onError mapper converts rejected values into typed errors:
const fetchUser = Task.from(
  () => fetch('/api/user').then((r) => r.json()) as Promise<User>,
  (e) => new Error(`Failed: ${e}`)
);
// Task<User, Error>

Task.ask<Context>()

Creates a task that returns the context provided via .provide(). Use this for dependency injection:
interface AppContext {
  apiUrl: string;
  authToken: string;
}

const fetchUsers = Task.ask<AppContext>().chain((ctx) =>
  Task.from(
    () =>
      fetch(`${ctx.apiUrl}/users`, {
        headers: { Authorization: ctx.authToken },
      }).then((r) => r.json()),
    (e) => new Error(`Fetch failed: ${e}`)
  )
);

// Inject context at the call site
const result = await fetchUsers
  .provide({ apiUrl: 'https://api.example.com', authToken: 'Bearer abc' })
  .execute();

Chaining

.chain(fn)

Chains a function that returns another Task. Errors short-circuit automatically:
const pipeline = Task.from(() => fetchRawData())
  .chain((data) => Task.from(() => processData(data)))
  .chain((processed) => Task.from(() => saveResult(processed)));

.map(fn) and .mapErr(fn)

Transform the value or error without creating a new async operation:
const userNameTask = Task.from(() => fetchUser())
  .map((user) => user.name)
  .mapErr((e) => `Could not get name: ${e.message}`);

Resilience

Chain resilience operators before calling .execute(). They compose cleanly and are applied in order:
const resilientFetch = Task.from(
  () => fetch('/api/data').then((r) => r.json()),
  (e) => new Error(`${e}`)
)
  .retry(3, { delay: 1000, factor: 2 })      // up to 3 retries, exponential backoff
  .timeout(5000, () => new Error('Timed out')) // fail fast after 5 seconds
  .circuitBreaker({ threshold: 5, resetTimeout: 30_000 }); // open after 5 failures

.retry(count, options)

Retries the task up to count times on failure. Pass delay (ms) and factor for exponential backoff:
task.retry(3, { delay: 500, factor: 2 });
// Waits: 500ms → 1000ms → 2000ms between attempts

.timeout(ms, onTimeout)

Fails with a custom error if the task takes longer than ms milliseconds:
task.timeout(3000, () => new Error('Request timed out'));

.circuitBreaker({ threshold, resetTimeout })

Opens the circuit after threshold consecutive failures and rejects all calls until resetTimeout ms have elapsed:
task.circuitBreaker({ threshold: 5, resetTimeout: 60_000 });

.throttle(concurrency)

Limits the number of concurrent executions of this task:
const uploadTask = Task.from(() => uploadFile(file)).throttle(3);
// At most 3 uploads run at once

.fallback(otherTask)

Runs otherTask if the primary task fails:
const primaryTask = Task.from(() => fetch('https://primary.example.com/data'));
const backupTask  = Task.from(() => fetch('https://backup.example.com/data'));

const result = await primaryTask.fallback(backupTask).execute();

Recovery

.orElse(fn)

Recover from any error by returning a new Task:
const result = await fetchFromRemote()
  .orElse(() => Task.from(() => loadFromDisk()))
  .execute();

.catchTag(errorFactory, handler)

Catch a specific tagged error variant. It is removed from the error union type after handling:
import { chas } from 'ts-chas';

const AppError = chas.defineErrs({
  NotFound: (resource: string) => ({ resource }),
  Unauthorized: () => ({}),
});

const result = await fetchUser(id)
  .catchTag(AppError.NotFound, (e) =>
    Task.from(() => Promise.resolve(GUEST_USER))
  )
  .execute();
// Task<User, Unauthorized> — NotFound is no longer in the error type

Side effects

const result = await fetchUser(id)
  .tap((user) => console.log('Got user:', user.name))    // runs on success
  .tapErr((e) => console.error('Failed:', e.message))    // runs on failure
  .execute();

Execution control

.delay(ms)

Waits ms milliseconds before executing:
task.delay(500).execute(); // waits 500ms then runs

.withSignal(abortSignal)

Cancels the task if the signal is aborted:
const controller = new AbortController();
const result = await task.withSignal(controller.signal).execute();

// Elsewhere: controller.abort();

.once()

Executes only once and caches the result for all subsequent calls:
const initTask = Task.from(() => initializeApp()).once();

await initTask.execute(); // runs the init
await initTask.execute(); // returns cached result immediately

.memoize({ ttl? })

Like .once(), but supports a time-to-live in milliseconds after which the cache expires:
const userTask = Task.from(() => fetchUser(id))
  .memoize({ ttl: 60_000 }); // cached for 1 minute

.cache(key, store)

Uses a custom cache store implementing the TaskCache interface. Useful for Redis, IndexedDB, or any external store:
import type { TaskCache } from 'ts-chas/task';

const redisCache: TaskCache = {
  get: async (key) => redis.get(key).then((v) => (v ? JSON.parse(v) : undefined)),
  set: async (key, value, ttl) =>
    redis.set(key, JSON.stringify(value), 'PX', ttl ?? 60_000),
};

const result = await task.cache('user:123', redisCache, { ttl: 30_000 }).execute();

Context/DI with Task.ask()

Task.ask() combined with .provide() is a lightweight dependency injection pattern that keeps your tasks testable and decoupled:
interface DbContext {
  dbUrl: string;
}

const queryTask = Task.ask<DbContext>().chain((ctx) =>
  Task.from(
    () => db.connect(ctx.dbUrl).then((conn) => conn.query('SELECT * FROM users')),
    (e) => new Error(`DB error: ${e}`)
  )
);

// In production
const result = await queryTask
  .provide({ dbUrl: process.env.DATABASE_URL! })
  .execute();

// In tests
const testResult = await queryTask
  .provide({ dbUrl: 'postgresql://localhost/test_db' })
  .execute();

Resource management

Task.using() acquires a resource, runs a task, and guarantees the resource is released, even if the task fails:
const result = await Task.using(
  Task.from(() => db.getConnection(), (e) => new Error(`${e}`)), // acquire
  (conn) => conn.release(),                                        // release
  (conn) =>
    Task.from(
      () => conn.query('SELECT * FROM users'),
      (e) => new Error(`Query failed: ${e}`)
    )
).execute();

Parallel execution

const tasks = [fetchUser('1'), fetchUser('2'), fetchUser('3')];

// All must succeed (short-circuits on first Err)
const allResult = await Task.all(tasks).execute();
// Result<User[], Error>

// Succeeds with the first Ok, or Err with all errors
const anyResult = await Task.any(tasks).execute();
// Result<User, Error[]>

// Collects all errors instead of short-circuiting
const collectResult = await Task.collect(tasks).execute();
// Result<User[], Error[]>

// Resolves with whichever completes first
const raceResult = await Task.race(tasks).execute();
// Result<User, Error>

// Run with a concurrency limit
const parallelResult = await Task.parallel(tasks, 2).execute();
// At most 2 tasks run at once

Do-notation with Task.go()

Use Task.go() to write sequential task pipelines that look imperative, without nesting .chain() calls:
const userProfileTask = Task.go(async function* () {
  const user    = yield* fetchUserTask(id);      // short-circuits on Err
  const posts   = yield* fetchPostsTask(user.id);
  const friends = yield* fetchFriendsTask(user.id);
  return { user, posts, friends };
});

const result = await userProfileTask.execute();
// Result<{ user: User; posts: Post[]; friends: User[] }, ApiError>
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.