π‘οΈ 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
}- π― Exhaustive matching - TypeScript enforces that you handle all error types
- π§ Ergonomic API - Declarative
matchError/matchErrorOfchains with:.map()for error transformation.select()for property extraction.withAny()for matching multiple types.withNot()for negation patterns.when()for predicate matching
- π¦ Tiny & fast - ~5 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()andmatchErrorOfAsync() - πΎ Serialization - JSON serialization with
serialize(),deserialize(),toJSON(),fromJSON()
npm install ts-typed-errorsimport { 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);
}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
}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' }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
}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}`);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 missingAsync 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 handledTransform 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
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
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
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
Type guard to check if a value is an Error instance.
if (isError(value)) {
// value is Error
}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');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');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) {
// ...
}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');
}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)
});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!
}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]);// 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();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;
};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
MIT Β© Quentin Ackermann