Error Handling Patterns
Handle errors systematically: Result types vs exceptions, error boundaries, structured error hierarchies, retry logic, and circuit breakers in TypeScript.
Error handling is a cross-cutting concern that determines system resilience. Ad-hoc error handling leads to unhandled errors, swallowed exceptions, and brittle code. Systematic patterns - typed errors, Result types, circuit breakers, and structured logging - build fault-tolerant systems.
Use typed errors for expected failures (validation, not-found, unauthorized). Reserve exceptions for unexpected failures. Always handle rejected Promises - `floating-promises` ESLint rule. Structure errors consistently for logging and client responses.
Workflow
- Design a structured error hierarchyStructured errors enable consistent handling, logging, and client responses.
- Use Result types for expected failuresResult types make error handling explicit at compile time - callers can't ignore errors.
- Implement retry with exponential backoffTransient failures (network blips, rate limits) should be retried - not surfaced to users.
- Add circuit breaker protectionCircuit breakers prevent cascade failures when a service is degraded - fail fast instead of queueing requests.
- Handle async errors correctlyUnhandled Promise rejections crash Node.js processes and cause silent failures in browsers.
Output Format
AppError hierarchy with code/statusCode, Result type for expected failures, `withRetry` with jitter and shouldRetry predicate, CircuitBreaker class, Express global error handler, `unhandledRejection` process listener.
Examples
neverthrow: `import { ResultAsync } from "neverthrow"; const fetchUser = (id: string) => ResultAsync.fromPromise(db.users.findById(id), (e) => new DatabaseError(e)); fetchUser("123").map(user => user.name).mapErr(err => { logger.error(err); return "unknown" }).match(name => name, () => "error")`Retry with circuit breaker: `const breaker = new CircuitBreaker(() => externalAPI.call(), { threshold: 5, resetTimeout: 30000 }); const result = await withRetry(() => breaker.call(), { maxAttempts: 3, baseDelay: 500, shouldRetry: (e) => !(e instanceof CircuitOpenError) })`Floating promise: `// BAD: await sendEmail(user.email) // forgot await - unhandled rejection; // GOOD: await sendEmail(user.email) OR void sendEmail(user.email).catch(logger.error) // explicit discard`Edge Cases & Gotchas
- Error serialization: `Error` objects don't serialize to JSON (only `message`, not `stack` or custom properties). Use `JSON.stringify(err, Object.getOwnPropertyNames(err))` or a custom serializer for logging
- Error in error handler: if the error handler itself throws, Express sends a 500 with no body. Always wrap your error handler in a try/catch
- Async iterators and errors: errors in `for await...of` loops propagate to the enclosing async function. Use try/catch around the loop, not inside it, for cleanup
When to Break These Rules
- For simple scripts and CLIs, `try/catch` with `console.error` is sufficient. Structured error hierarchies and circuit breakers are production service concerns.
Reference Files
neverthrow: https://github.com/supermacro/neverthrowopossum circuit breaker: https://github.com/nodeshift/opossumNode.js error handling: https://nodejs.org/en/docs/guides/error-handlingOWASP error handling: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html