Skip to main content
Tagged errors are native Error instances extended with a _tag discriminant. They let you build discriminated unions of errors that TypeScript can check exhaustively — and that work with real stack traces, console.log, and error monitoring services.
import { chas } from 'ts-chas';

// Without tagged errors: error is `unknown`
try {
  await fetchUser(id);
} catch (e) {
  e; // unknown — you have no idea what went wrong
}

// With tagged errors: errors are in the type signature
const result = await fetchUser(id);
// Result<User, NotFoundErr | UnauthorizedErr>

Defining errors

chas.defineErrs() takes an object where each key becomes a _tag and the value is a factory function that returns the error’s extra data:
import { chas } from 'ts-chas';

const AppError = chas.defineErrs({
  NotFound:     (resource: string, id?: string) => ({ resource, id }),
  Unauthorized: (userId: string) => ({ userId }),
  Validation:   (field: string, message: string) => ({ field, message }),
});
Each factory produces a real Error instance with:
  • A _tag field matching the key name
  • A name field set to the tag
  • A full stack trace
  • All the data properties you returned from the factory

Creating errors

Call the factory directly, or use the .err() helper to wrap it in a Result:
// Create the error instance
const e = AppError.NotFound('user', '123');
// Error { _tag: 'NotFound', name: 'NotFound', resource: 'user', id: '123', stack: '...' }

// Create an Err result in one call
const result = AppError.NotFound.err('user', '123');
// Result<never, NotFoundErr>

// Async variant
const asyncResult = AppError.NotFound.errAsync('user', '123');
// ResultAsync<never, NotFoundErr>

Inferring types

// Union of all error variants
type AppErr = chas.InferErrs<typeof AppError>;
// NotFoundErr | UnauthorizedErr | ValidationErr

// A single variant
type NotFoundErr = chas.InferErr<typeof AppError.NotFound>;
// { _tag: 'NotFound'; resource: string; id?: string } & Error

// Extract a specific variant from a union
type Auth = chas.ExtractErr<AppErr, 'Unauthorized'>;

Using errors in function signatures

function fetchUser(id: string): chas.ResultAsync<User, AppErr> {
  if (!isAuthenticated()) {
    return AppError.Unauthorized.errAsync(currentUserId);
  }
  return chas.fromPromise(
    db.users.find(id),
    () => AppError.NotFound('user', id)
  );
}

Exhaustive matching

chas.matchErr() requires a handler for every variant. TypeScript will error if you miss one:
const result = await fetchUser(id);

if (result.isErr()) {
  const message = chas.matchErr(result.unwrapErr(), {
    NotFound:     (e) => `${e.resource} with id ${e.id} was not found`,
    Unauthorized: (e) => `User ${e.userId} lacks permission`,
    Validation:   (e) => `${e.field}: ${e.message}`,
  });
  console.error(message);
}

Partial matching

chas.matchErrPartial() lets you handle only the variants you care about. The _ handler catches everything else and receives the remaining union type:
const message = chas.matchErrPartial(error, {
  NotFound: (e) => `Could not find ${e.resource}`,
  _: (e) => `Something went wrong: ${e.message}`,
  // `e` in `_` is typed as UnauthorizedErr | ValidationErr
});

Catching a specific tag mid-chain

.catchTag() handles a single variant inline, removing it from the error union. The remaining chain has a narrower error type:
const result = await fetchUser(id)
  .catchTag(AppError.NotFound, (e) => {
    console.log(`No ${e.resource} found, using guest`);
    return chas.ok(GUEST_USER);
  });
// Result<User, UnauthorizedErr | ValidationErr>
// NotFoundErr is no longer in the union
You can also match by tag string instead of factory:
.catchTag('NotFound', (e) => chas.ok(GUEST_USER))
// Note: `e` is less precisely typed when using a string

Tapping a tag without catching

.tapTag() runs a side effect on a specific variant but leaves the result unchanged:
const result = await fetchUser(id)
  .tapTag(AppError.NotFound, (e) => {
    analytics.track('resource_not_found', { resource: e.resource });
  });
// Result<User, NotFoundErr | UnauthorizedErr> — unchanged

Guard integration

Use is.tagged() to check an unknown value against a tagged error factory:
import { is } from 'ts-chas/guard';

function handleError(e: unknown) {
  if (AppError.NotFound.is(e)) {
    // e is narrowed to NotFoundErr
    console.log(`Missing: ${e.resource}`);
  }
}
Each error factory has a .is(value) method as a convenience type guard. You can also use is.tagged(AppError.NotFound) from ts-chas/guard for the same effect.

Error wrapping with cause

If your factory data includes a cause property that is an Error, it is set as the native Error.cause, creating a proper error chain:
const AppError = chas.defineErrs({
  Http: (status: number, cause?: Error) => ({ status, cause }),
});

const inner = new Error('Connection refused');
const outer = AppError.Http(503, inner);

outer.cause === inner; // true — full chain preserved