> ## 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.

# Tagged Errors

> Discriminated error unions for exhaustive matching.

Tagged errors give you typed, discriminated `Error` instances that work naturally with `Result`, `Task`, and TypeScript's type system. Each error has a `_tag` discriminant, extends the native `Error` class, and can be matched exhaustively — no more `instanceof` chains or untyped `catch` blocks.

```typescript theme={null}
import { chas } from 'ts-chas';
// or, for tree-shaking:
import { defineErrs, matchErr, matchErrPartial } from 'ts-chas/tagged-errs';
```

***

## `chas.defineErrs(factories, baseProps?)`

Defines a set of typed error factories. Each key becomes the `_tag` discriminant. The factory function returns extra properties that are merged onto the `Error` instance.

```typescript theme={null}
const AppError = chas.defineErrs({
  NotFound:     (resource: string, id: string) => ({ resource, id }),
  Validation:   (field: string, message: string) => ({ field, message }),
  Unauthorized: () => ({}),
  Http:         (status: number, cause?: Error) => ({ status, cause }),
});
```

Each factory produces a real `Error` instance:

```typescript theme={null}
const e = AppError.NotFound('user', '123');
// {
//   _tag: 'NotFound',
//   name: 'NotFound',
//   message: '[NotFound]',
//   resource: 'user',
//   id: '123',
//   stack: '...',
// }

e instanceof Error; // true
```

If the factory data includes a `message` or `msg` string, it becomes the error's message. If it includes a `cause` that is an `Error`, it is set as `Error.cause` for native error wrapping.

### `baseProps`

Pass a second argument to add shared properties to every error in the set:

```typescript theme={null}
const HttpError = chas.defineErrs(
  {
    NotFound:   (url: string) => ({ url }),
    BadGateway: (upstream: string) => ({ upstream }),
  },
  { service: 'payments-api' } // added to every error
);

const e = HttpError.NotFound('/checkout');
e.service; // 'payments-api'
```

***

## Factory shortcuts

Each factory in the returned object also carries three shortcut methods:

### `.err()`

Creates a `Result.Err` wrapping the error. Saves you from writing `chas.err(AppError.NotFound(...))`.

```typescript theme={null}
function findUser(id: string): Result<User, NotFoundErr> {
  if (!db.has(id)) {
    return AppError.NotFound.err('user', id);
    // equivalent to: return chas.err(AppError.NotFound('user', id))
  }
  return chas.ok(db.get(id)!);
}
```

### `.errAsync()`

Creates a `ResultAsync.Err` wrapping the error:

```typescript theme={null}
async function findUserAsync(id: string): ResultAsync<User, NotFoundErr> {
  if (!db.has(id)) {
    return AppError.NotFound.errAsync('user', id);
  }
  return chas.okAsync(db.get(id)!);
}
```

### `.is(value)`

Type guard that checks whether an unknown value is this specific error:

```typescript theme={null}
const e: unknown = someError;

if (AppError.NotFound.is(e)) {
  console.log(e.resource, e.id); // fully typed
}
```

***

## Type inference

### `chas.InferErrs<typeof AppError>`

Extracts the discriminated union of all error types from an error factory object.

```typescript theme={null}
const AppError = chas.defineErrs({
  NotFound:   (resource: string) => ({ resource }),
  Validation: (field: string) => ({ field }),
});

type AppErr = chas.InferErrs<typeof AppError>;
// { _tag: 'NotFound'; resource: string; ... } | { _tag: 'Validation'; field: string; ... }
```

### `chas.InferErr<typeof AppError.NotFound>`

Extracts a single error variant's type from a factory function.

```typescript theme={null}
type NotFoundErr = chas.InferErr<typeof AppError.NotFound>;
// { _tag: 'NotFound'; resource: string; ... }
```

***

## `chas.matchErr(error, handlers)`

Exhaustively matches on a tagged error. TypeScript enforces that every `_tag` variant is handled — a compile error if you miss one.

```typescript theme={null}
import { chas } from 'ts-chas';

const message = chas.matchErr(error, {
  NotFound:     (e) => `Could not find ${e.resource} with id ${e.id}`,
  Validation:   (e) => `Validation failed on "${e.field}": ${e.message}`,
  Unauthorized: ()  => 'You do not have permission.',
  Http:         (e) => `HTTP ${e.status} from upstream service`,
});
// TypeScript error if any tag is missing from handlers
```

***

## `chas.matchErrPartial(error, handlers)`

Partial matching with a required `_` wildcard fallback. Use when you only care about specific variants and want a catch-all for the rest.

```typescript theme={null}
const message = chas.matchErrPartial(error, {
  Validation:   (e) => `Invalid ${e.field}`,
  Unauthorized: ()  => 'Please log in',
  _: (e) => `Unexpected error: ${e._tag}`,
});
```

***

## `.catchTag(target, handler)` on Task / Result

Catches one specific tagged error from a `Task` or `Result` chain, handles it, and removes it from the error union type. The remaining error union is automatically narrowed.

`target` can be a factory reference (`AppError.NotFound`) or a plain string (`'NotFound'`).

```typescript theme={null}
// fetchUser returns Task<User, NotFoundErr | UnauthorizedErr>
const result = await fetchUser('123')
  .catchTag(AppError.NotFound, e => {
    console.log(`User ${e.id} not found, returning guest`);
    return Task.from(() => Promise.resolve(guestUser));
  })
  .execute();
// Task<User, UnauthorizedErr>  — NotFoundErr is gone from the type
```

***

## `.tapTag(target, handler)` on Task / Result

Side-effect on a specific tagged error. The result passes through unchanged — the error is not caught.

```typescript theme={null}
const result = await fetchUser('123')
  .tapTag(AppError.NotFound, e => {
    metrics.increment('user.not_found', { resource: e.resource });
  })
  .execute();
// Result<User, NotFoundErr | UnauthorizedErr>  — unchanged
```

***

## Complete flow

```typescript theme={null}
import { chas } from 'ts-chas';
import { Task } from 'ts-chas/task';

// 1. Define your error set
const AppError = chas.defineErrs({
  NotFound:     (resource: string, id: string) => ({ resource, id }),
  Unauthorized: () => ({}),
  Database:     (message: string, cause?: Error) => ({ message, cause }),
});

type AppErr = chas.InferErrs<typeof AppError>;

// 2. Use them in your functions
function getUser(id: string): Task<User, AppErr> {
  return Task.from(async () => {
    const session = getSession();
    if (!session) throw AppError.Unauthorized();

    const user = await db.users.findById(id);
    if (!user) throw AppError.NotFound('user', id);

    return user;
  }, (e) => {
    if (AppError.NotFound.is(e) || AppError.Unauthorized.is(e)) return e as AppErr;
    return AppError.Database(`DB error: ${e}`) as AppErr;
  });
}

// 3. Handle errors exhaustively at the call site
const result = await getUser('42')
  .tapTag(AppError.Database, e => logger.error('DB error', e))
  .catchTag(AppError.NotFound, e =>
    Task.from(() => Promise.resolve(defaultUser))
  )
  .execute();

if (result.isErr()) {
  // Only UnauthorizedErr and DatabaseErr remain in the union at this point
  const message = chas.matchErr(result.error, {
    Unauthorized: () => 'Please log in to continue.',
    Database: (e) => `Database issue: ${e.message}`,
  });
  showError(message);
} else {
  renderUser(result.value);
}
```
