> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cbrock.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Task

> Lazy, resilient async pipelines.

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.

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
task.circuitBreaker({ threshold: 5, resetTimeout: 60_000 });
```

### `.throttle(concurrency)`

Limits the number of concurrent executions of this task:

```typescript theme={null}
const uploadTask = Task.from(() => uploadFile(file)).throttle(3);
// At most 3 uploads run at once
```

### `.fallback(otherTask)`

Runs `otherTask` if the primary task fails:

```typescript theme={null}
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`:

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
task.delay(500).execute(); // waits 500ms then runs
```

### `.withSignal(abortSignal)`

Cancels the task if the signal is aborted:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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:

```typescript theme={null}
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

```typescript theme={null}
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:

```typescript theme={null}
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>
```

<Tip>
  `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.
</Tip>
