Skip to main content

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 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.
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.
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:
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:
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(...)).
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:
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:
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.
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.
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.
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.
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').
// 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.
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

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);
}