npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ts-typed-errors

v0.5.0

Published

Exhaustive error matching utilities for TypeScript (defineError, matchError, matchErrorOf, wrap)

Readme

ts-typed-errors

npm version bundle size CI

🛡️ Exhaustive error matching for TypeScript - tiny, dependency-free, type-safe.

import { defineError, matchErrorOf, wrap } from 'ts-typed-errors';

const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();
const ParseError   = defineError('ParseError')<{ at: string }>();

type Err = InstanceType<typeof NetworkError> | InstanceType<typeof ParseError>;

const safeJson = wrap(async (url: string) => {
  const r = await fetch(url);
  if (!r.ok) throw new NetworkError(`HTTP ${r.status}`, { status: r.status, url });
  try { return await r.json(); }
  catch { throw new ParseError('Invalid JSON', { at: url }); }
});

const res = await safeJson('https://httpstat.us/404');
if (!res.ok) {
  return matchErrorOf<Err>(res.error)
    .with(NetworkError, e => `retry ${e.data.url}`)
    .with(ParseError,   e => `report ${e.data.at}`)
    .exhaustive(); // ✅ TypeScript ensures all cases are covered
}

✨ Features

  • 🎯 Exhaustive matching - TypeScript enforces that you handle all error types
  • 🔧 Ergonomic API - Declarative matchError / matchErrorOf chains with:
    • .map() for error transformation
    • .select() for property extraction
    • .withAny() for matching multiple types
    • .withNot() for negation patterns
    • .when() for predicate matching
  • 📦 Tiny & fast - ~6.4 kB, zero dependencies, O(1) tag-based matching
  • 🛡️ Type-safe - Full TypeScript support with strict type checking
  • 🔄 Result pattern - Convert throwing functions to Result<T, E> types
  • 🔨 Composable guards - Reusable type guards with isErrorOf(), isAnyOf(), isAllOf()
  • ⚡ Async support - Native async/await with matchErrorAsync() and matchErrorOfAsync()
  • 💾 Serialization - JSON serialization with serialize(), deserialize(), toJSON(), fromJSON()

🚀 Quick Start

Installation

npm install ts-typed-errors

Basic Usage

import { defineError, matchErrorOf, wrap } from 'ts-typed-errors';

// 1. Define your error types
const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();
const ValidationError = defineError('ValidationError')<{ field: string; value: any }>();

type AppError = InstanceType<typeof NetworkError> | InstanceType<typeof ValidationError>;

// 2. Wrap throwing functions
const safeFetch = wrap(async (url: string) => {
  const response = await fetch(url);
  if (!response.ok) {
    throw new NetworkError(`HTTP ${response.status}`, { 
      status: response.status, 
      url 
    });
  }
  return response.json();
});

// 3. Handle errors exhaustively
const result = await safeFetch('https://api.example.com/data');
if (!result.ok) {
  const message = matchErrorOf<AppError>(result.error)
    .with(NetworkError, e => `Network error: ${e.data.status} for ${e.data.url}`)
    .with(ValidationError, e => `Invalid ${e.data.field}: ${e.data.value}`)
    .exhaustive(); // ✅ Compiler ensures all cases covered
  
  console.log(message);
}

📚 What is Exhaustive Error Matching?

Since TypeScript 4.4, every catch block receives an unknown type. This means you need to manually narrow error types with verbose if/else blocks:

// ❌ Verbose and error-prone
try {
  await riskyOperation();
} catch (error) {
  if (error instanceof NetworkError) {
    // handle network error
  } else if (error instanceof ValidationError) {
    // handle validation error
  } else {
    // handle unknown error
  }
}

ts-typed-errors makes this ergonomic and type-safe:

// ✅ Clean and exhaustive
const result = await wrap(riskyOperation)();
if (!result.ok) {
  return matchErrorOf<AllErrors>(result.error)
    .with(NetworkError, handleNetwork)
    .with(ValidationError, handleValidation)
    .exhaustive(); // Compiler ensures you handle all cases
}

🔧 API Reference

Core Functions

defineError(name)<Data>()

Creates a typed error class with optional data payload.

const UserError = defineError('UserError')<{ userId: string; reason: string }>();
const error = new UserError('User not found', { userId: '123', reason: 'deleted' });
// error.tag === 'UserError'
// error.data === { userId: '123', reason: 'deleted' }

wrap(fn)

Converts a throwing function to return Result<T, E>.

const safeJson = wrap(async (url: string) => {
  const response = await fetch(url);
  if (!response.ok) throw new Error('HTTP error');
  return response.json();
});

const result = await safeJson('https://api.example.com');
if (result.ok) {
  console.log(result.value); // T
} else {
  console.log(result.error); // Error
}

matchError(error)

Free matcher for any error type. Always requires .otherwise().

const message = matchError(error)
  .with(NetworkError, e => `Network: ${e.data.status}`)
  .with(ValidationError, e => `Validation: ${e.data.field}`)
  .otherwise(e => `Unknown: ${e.message}`);

matchErrorOf<AllErrors>(error)

Exhaustive matcher that ensures all error types are handled.

type AllErrors = NetworkError | ValidationError | ParseError;

const message = matchErrorOf<AllErrors>(error)
  .with(NetworkError, e => `Network: ${e.data.status}`)
  .with(ValidationError, e => `Validation: ${e.data.field}`)
  .with(ParseError, e => `Parse: ${e.data.at}`)
  .exhaustive(); // ✅ Compiler error if any case missing

matchErrorAsync(error) & matchErrorOfAsync<AllErrors>(error)

Async versions with native async/await support for all handlers.

// Free-form async matching
const result = await matchErrorAsync(error)
  .with(NetworkError, async (err) => {
    await logToService(err);
    return `Logged network error: ${err.data.status}`;
  })
  .with(ParseError, async (err) => {
    await notifyAdmin(err);
    return `Notified admin about parse error`;
  })
  .otherwise(async (err) => `Unknown error: ${err}`);

// Exhaustive async matching
const result = await matchErrorOfAsync<AllErrors>(error)
  .with(NetworkError, async (err) => {
    await retryRequest(err);
    return 'retried';
  })
  .with(ValidationError, async (err) => {
    await validateAndLog(err);
    return 'validation';
  })
  .with(ParseError, async (err) => {
    await fixData(err);
    return 'fixed';
  })
  .exhaustive(); // ✅ All cases handled

Advanced Matching

Pattern Builders (P namespace)

Compose complex matching patterns with the P namespace, inspired by ts-pattern.

P.union() - Match any pattern
import { P, matchError } from 'ts-typed-errors';

const NetworkError = defineError('NetworkError')<{ status: number }>();
const TimeoutError = defineError('TimeoutError')<{ duration: number }>();

matchError(error)
  .with(
    P.union(P.instanceOf(NetworkError), P.instanceOf(TimeoutError)),
    e => 'Connection issue - retry'
  )
  .otherwise(() => 'Unknown error');
P.intersection() - Match all patterns
// Match NetworkError with status >= 500
matchError(error)
  .with(
    P.intersection(
      P.instanceOf(NetworkError),
      P.when(e => e.data.status >= 500)
    ),
    e => `Server error: ${e.data.status}`
  )
  .otherwise(() => 'Other error');
Pattern Composition
// Reusable patterns
const isServerError = P.intersection(
  P.instanceOf(NetworkError),
  P.when(e => e.data.status >= 500)
);

const isCritical = P.union(
  isServerError,
  P.instanceOf(DatabaseError)
);

// Use anywhere
matchError(error)
  .with(isCritical, () => 'ALERT: Critical error')
  .otherwise(() => 'Normal error');
Error-Specific Patterns

Match on error-specific properties like arrays, causes, and stack traces:

const ValidationError = defineError('ValidationError')<{
  errors: Array<{ field: string; message: string }>
}>();

// Match on array properties
matchError(error)
  .with(
    P.intersection(
      P.instanceOf(ValidationError),
      P.array('errors', P.when(e => e.field === 'email'))
    ),
    () => 'Email validation failed'
  )
  .otherwise(() => 'Other error');

// Match by error cause chain
const rootCause = new NetworkError('failed', { status: 500, url: '/api' });
const wrapped = Object.assign(new Error('wrapped'), { cause: rootCause });

matchError(wrapped)
  .with(P.hasCause(NetworkError), () => 'Network issue in cause chain')
  .otherwise(() => 'Other');

// Match by stack trace
matchError(error)
  .with(P.hasStack(/internal/), () => 'Internal error')
  .with(P.hasStack(/node_modules/), () => 'Third-party error')
  .otherwise(() => 'Application error');

Available pattern builders:

  • P.instanceOf(Constructor) - Match by error constructor
  • P.when(predicate) - Match with custom predicate
  • P.guard(guardFn) - Match using type guard function
  • P.union(...patterns) - Match any of the patterns
  • P.intersection(...patterns) - Match all patterns
  • P.not(pattern) - Negate a pattern
  • P.array(key, itemPattern) - Match array properties (for AggregateError, ValidationError)
  • P.optional(key) - Match optional properties
  • P.nullish(key) - Match null/undefined properties
  • P.hasCause(Constructor) - Match by error cause chain
  • P.hasStack(pattern) - Match by stack trace pattern

.map(transform)

Transform the error before matching against it. Useful for normalizing errors or adding context.

const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();
const ParseError = defineError('ParseError')<{ at: string }>();

// Normalize errors by adding a timestamp
matchErrorOf<Err>(error)
  .map(e => {
    (e as any).timestamp = Date.now();
    return e;
  })
  .with(NetworkError, e => `Network error at ${(e as any).timestamp}`)
  .with(ParseError, e => `Parse error at ${(e as any).timestamp}`)
  .exhaustive();

// Extract nested errors
matchError(wrappedError)
  .map(e => (e as any).cause ?? e)
  .with(NetworkError, e => `Root cause: ${e.data.status}`)
  .otherwise(() => 'Unknown error');

Benefits:

  • Error normalization across different sources
  • Extract nested/wrapped errors
  • Add contextual information
  • Works with both exhaustive and non-exhaustive matching

.select(constructor, key, handler)

Extract and match on specific properties from error data directly.

const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();
const ParseError = defineError('ParseError')<{ at: string }>();

// Extract specific property instead of full error object
matchErrorOf<Err>(error)
  .select(NetworkError, 'status', (status) => `Status code: ${status}`)
  .select(ParseError, 'at', (location) => `Parse failed at: ${location}`)
  .exhaustive();

// Mix with regular .with() handlers
matchError(error)
  .select(NetworkError, 'status', (status) => status > 400 ? 'client error' : 'ok')
  .with(ParseError, (e) => `Parse error at ${e.data.at}`)
  .otherwise(() => 'unknown');

Benefits:

  • Cleaner handler signatures
  • Direct access to needed properties
  • Type-safe property extraction
  • Works with exhaustive matching

.withAny(constructors, handler)

Match multiple error types with the same handler.

const NetworkError = defineError('NetworkError')<{ status: number }>();
const TimeoutError = defineError('TimeoutError')<{ duration: number }>();
const ParseError = defineError('ParseError')<{ at: string }>();

matchErrorOf<Err>(error)
  .withAny([NetworkError, TimeoutError], (e) => 'Connection issue - retry')
  .with(ParseError, (e) => `Parse error at ${e.data.at}`)
  .exhaustive();

Benefits:

  • DRY principle - avoid duplicating handlers
  • Group similar error types together
  • Cleaner code for common error handling

.withNot(constructor | constructors, handler)

Match all errors except the specified types.

// Exclude single type
matchError(error)
  .withNot(NetworkError, (e) => 'Not a network error')
  .otherwise(() => 'Network error');

// Exclude multiple types
matchError(error)
  .withNot([NetworkError, ParseError], (e) => 'Neither network nor parse error')
  .otherwise((e) => 'Fallback');

Benefits:

  • Handle "everything except X" scenarios
  • Reduce boilerplate for common cases
  • More expressive API

Utility Functions

isError(value)

Type guard to check if a value is an Error instance.

if (isError(value)) {
  // value is Error
}

hasCode(code)

Creates a type guard for errors with a specific error code.

const isDNSError = hasCode('ENOTFOUND');
const isPermissionError = hasCode('EACCES');

if (isDNSError(error)) {
  // Handle DNS error - TypeScript knows error.code is 'ENOTFOUND'
}

// Use in pattern matching
matchError(error)
  .with(hasCode('ENOTFOUND'), (err) => 'DNS lookup failed')
  .with(hasCode('EACCES'), (err) => 'Permission denied')
  .otherwise((err) => 'Other error');

isErrorOf(constructor, predicate?)

Creates reusable type guards for specific error types with optional predicates.

const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();

// Simple type guard
const isNetworkError = isErrorOf(NetworkError);
if (isNetworkError(error)) {
  console.log(error.data.status); // TypeScript knows this is NetworkError
}

// Type guard with predicate
const isServerError = isErrorOf(NetworkError, (e) => e.data.status >= 500);
const isClientError = isErrorOf(NetworkError, (e) => e.data.status >= 400 && e.data.status < 500);

if (isServerError(error)) {
  console.log(`Server error: ${error.data.status}`);
}

// Use in pattern matching
matchError(error)
  .with(isServerError, (e) => 'Retry server error')
  .with(isClientError, (e) => 'Handle client error')
  .otherwise(() => 'Other error');

isAnyOf(error, constructors)

Checks if an error is an instance of any of the provided error constructors.

const NetworkError = defineError('NetworkError')<{ status: number }>();
const TimeoutError = defineError('TimeoutError')<{ duration: number }>();

if (isAnyOf(error, [NetworkError, TimeoutError])) {
  // Handle connection-related errors
  console.log('Connection issue detected');
}

// More concise than:
if (error instanceof NetworkError || error instanceof TimeoutError) {
  // ...
}

isAllOf(value, guards)

Checks if a value matches all of the provided type guards.

const NetworkError = defineError('NetworkError')<{ status: number; url: string }>();

const isServerError = isErrorOf(NetworkError, (e) => e.data.status >= 500);
const hasRetryableStatus = (e: unknown): e is any =>
  isError(e) && 'status' in e && [502, 503, 504].includes((e as any).status);

if (isAllOf(error, [isServerError, hasRetryableStatus])) {
  // Error is both a server error AND has a retryable status
  console.log('Retrying server error');
}

Serialization

serialize(error, includeStack?)

Serializes an error to a JSON-safe object for transmission or storage.

const error = new NetworkError('Request failed', { status: 500, url: '/api' });
const serialized = serialize(error);
// {
//   tag: 'NetworkError',
//   message: 'Request failed',
//   name: 'NetworkError',
//   data: { status: 500, url: '/api' },
//   stack: '...'
// }

// Send over network
await fetch('/api/log', {
  method: 'POST',
  body: JSON.stringify(serialized)
});

deserialize(serialized, constructors)

Deserializes a plain object back into an error instance.

// Receive from API
const response = await fetch('/api/errors/123');
const serialized = await response.json();

// Deserialize with known constructors
const error = deserialize(serialized, [NetworkError, ParseError]);

if (error instanceof NetworkError) {
  console.log(`Network error: ${error.data.status}`); // Type-safe!
}

toJSON(error) & fromJSON(json, constructors)

Convenience functions combining serialization with JSON stringify/parse.

// Convert to JSON string
const json = toJSON(error);

// Parse from JSON string
const restored = fromJSON(json, [NetworkError, ParseError]);

🎯 Advanced Examples

Custom Error Hierarchy

// Base error with common properties
const BaseError = defineError('BaseError')<{ code: string }>();

// Specific errors extending base
const DatabaseError = defineError('DatabaseError')<{ table: string; operation: string }>();
const AuthError = defineError('AuthError')<{ userId?: string; permission: string }>();

type AppError = InstanceType<typeof DatabaseError> | InstanceType<typeof AuthError>;

// Exhaustive matching with data access
const handleError = (error: AppError) => 
  matchErrorOf<AppError>(error)
    .with(DatabaseError, e => ({
      type: 'database',
      table: e.data.table,
      operation: e.data.operation,
      code: e.data.code
    }))
    .with(AuthError, e => ({
      type: 'auth',
      userId: e.data.userId,
      permission: e.data.permission,
      code: e.data.code
    }))
    .exhaustive();

Result Chaining

const processUser = async (id: string) => {
  const userResult = await safeGetUser(id);
  if (!userResult.ok) return userResult;
  
  const validateResult = await safeValidateUser(userResult.value);
  if (!validateResult.ok) return validateResult;
  
  const saveResult = await safeSaveUser(validateResult.value);
  return saveResult;
};

🏗️ Architecture

ts-typed-errors is built around these core concepts:

  • Typed Errors: Custom error classes with structured data
  • Result Pattern: Functions return Result<T, E> instead of throwing
  • Exhaustive Matching: Compiler-enforced error handling
  • Zero Dependencies: Works in any TypeScript environment

📖 Documentation

📄 License

MIT © Quentin Ackermann