Skip to main content
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);
}