Skip to main content

Overview

TaggedError is a factory function that creates custom error classes with a _tag discriminator property. This enables exhaustive pattern matching, type-safe error unions, and improved error handling with full type inference. Tagged errors integrate seamlessly with Result types and support cause chaining, JSON serialization, and runtime type guards.

Factory Function

TaggedError<Tag extends string>(
  tag: Tag
): <Props extends Record<string, unknown> = {}>() => TaggedErrorClass<Tag, Props>

Parameters

tag
string
required
The discriminator tag for this error class. Used for pattern matching and type narrowing.

Returns

Returns a function that creates a class constructor. Call it immediately with type parameters to get your error class:
class NotFoundError extends TaggedError("NotFoundError")<{
  id: string;
  message: string;
}>() {}

Instance Properties

_tag
string
required
The discriminator tag identifying this error type. Used for exhaustive pattern matching.
message
string
Optional error message. If included in Props, will be passed to Error constructor.
cause
unknown
Optional error cause. If included in Props, will be chained in the stack trace.
name
string
Error name, set to the tag value.
stack
string | undefined
Stack trace. When cause is an Error, its stack is appended with “Caused by:” prefix.

Static Methods

is()

Type guard for checking error instances.
static is(value: unknown): value is TaggedErrorInstance<Tag, Props>

Global Type Guard

TaggedError.is(value: unknown): value is AnyTaggedError
Checks if a value is ANY TaggedError instance:
if (TaggedError.is(error)) {
  console.log(error._tag); // string - any tag
}

Class-Specific Type Guard

Each error class has its own is() method:
if (NotFoundError.is(error)) {
  console.log(error.id); // Narrowed to NotFoundError
}

Instance Methods

toJSON()

Serializes the error to a plain object.
toJSON(): object
Returns: Object containing all properties including _tag, name, message, cause, and stack.

Basic Usage

Creating Tagged Errors

class NotFoundError extends TaggedError("NotFoundError")<{
  id: string;
  message: string;
}>() {}

class ValidationError extends TaggedError("ValidationError")<{
  field: string;
  message: string;
}>() {}

type AppError = NotFoundError | ValidationError;

Creating Instances

const notFound = new NotFoundError({
  id: "user-123",
  message: "User not found"
});

console.log(notFound._tag);    // "NotFoundError"
console.log(notFound.id);      // "user-123"
console.log(notFound.message); // "User not found"

With Result Types

function findUser(id: string): Result<User, NotFoundError> {
  const user = db.users.get(id);
  if (!user) {
    return Result.err(new NotFoundError({ id, message: `User ${id} not found` }));
  }
  return Result.ok(user);
}

Pattern Matching

Exhaustive Matching

Use matchError for type-safe exhaustive pattern matching:
import { matchError } from "better-result";

function handleError(error: AppError): string {
  return matchError(error, {
    NotFoundError: (e) => `Missing: ${e.id}`,
    ValidationError: (e) => `Invalid field: ${e.field}`
  });
}
matchError(error, {
  NotFoundError: (e) => `Missing: ${e.id}`,
  ValidationError: (e) => `Invalid: ${e.field}`
});

Partial Matching with Fallback

Use matchErrorPartial when you only want to handle specific error types:
import { matchErrorPartial } from "better-result";

function handleError(error: AppError): string {
  return matchErrorPartial(
    error,
    {
      NotFoundError: (e) => `Missing: ${e.id}`
      // Only handle NotFoundError
    },
    (e) => {
      // Fallback receives ValidationError only
      // Type is narrowed to exclude handled errors
      return `Other error: ${e._tag}`;
    }
  );
}
The fallback parameter’s type is automatically narrowed to exclude handled error types.

Advanced Features

Error Cause Chaining

Automatically chains causes in stack traces:
class DatabaseError extends TaggedError("DatabaseError")<{
  message: string;
  cause: unknown;
}>() {}

try {
  await db.query(sql);
} catch (cause) {
  throw new DatabaseError({
    message: "Failed to fetch user",
    cause
  });
}
Stack trace will include:
DatabaseError: Failed to fetch user
    at ...
Caused by:
  Error: Connection timeout
    at ...

No-Props Errors

Create errors without additional properties:
class TimeoutError extends TaggedError("TimeoutError")<{}>() {}

const timeout = new TimeoutError(); // No args required

Custom Constructors

Override the constructor for custom initialization logic:
class ParseError extends TaggedError("ParseError")<{
  message: string;
  input: string;
  line: number;
}>() {
  constructor(args: { input: string; line: number }) {
    const message = `Parse error at line ${args.line}`;
    super({ ...args, message });
  }
}

// Usage - message derived automatically
const error = new ParseError({ input: "...", line: 42 });
console.log(error.message); // "Parse error at line 42"

JSON Serialization

const error = new NotFoundError({ id: "123", message: "Not found" });

const json = error.toJSON();
// {
//   _tag: "NotFoundError",
//   name: "NotFoundError",
//   message: "Not found",
//   id: "123",
//   cause: undefined,
//   stack: "..."
// }

console.log(JSON.stringify(error));

Type Guards

Class-Specific Guards

function handleError(error: unknown) {
  if (NotFoundError.is(error)) {
    console.log(error.id); // Type is NotFoundError
    console.log(error._tag); // "NotFoundError"
  }
}

Generic TaggedError Guard

import { isTaggedError } from "better-result";

function logError(error: unknown) {
  if (isTaggedError(error)) {
    console.log(`Error: ${error._tag}`);
  }
}

Best Practices

Always include a message property in your error Props for better debugging.
  1. Use descriptive tags: Make tags match the class name for clarity
    class NotFoundError extends TaggedError("NotFoundError") // ✅ Good
    class NotFoundError extends TaggedError("NFE")          // ❌ Unclear
    
  2. Type error unions: Create union types for domain-specific errors
    type AuthError = InvalidCredentials | TokenExpired | UserNotFound;
    type ApiError = NetworkError | RateLimited | ServerError;
    
  3. Exhaustive matching: Use matchError instead of if/else chains
    // ✅ Exhaustive - TypeScript enforces all cases
    matchError(error, {
      NotFoundError: ...,
      ValidationError: ...
    });
    
    // ❌ Non-exhaustive - missing cases not caught
    if (error._tag === "NotFoundError") { ... }
    
  4. Include metadata: Add relevant context to error properties
    class ValidationError extends TaggedError("ValidationError")<{
      field: string;
      value: unknown;
      constraint: string;
      message: string;
    }>() {}
    

See Also