Software Engineering5 min read

Error Handling Patterns

Handle errors systematically: Result types vs exceptions, error boundaries, structured error hierarchies, retry logic, and circuit breakers in TypeScript.

error-handlingtypescriptresiliencepatterns

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

  1. Design a structured error hierarchyStructured errors enable consistent handling, logging, and client responses.
  2. Use Result types for expected failuresResult types make error handling explicit at compile time - callers can't ignore errors.
  3. Implement retry with exponential backoffTransient failures (network blips, rate limits) should be retried - not surfaced to users.
  4. Add circuit breaker protectionCircuit breakers prevent cascade failures when a service is degraded - fail fast instead of queueing requests.
  5. 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/neverthrow
  • opossum circuit breaker: https://github.com/nodeshift/opossum
  • Node.js error handling: https://nodejs.org/en/docs/guides/error-handling
  • OWASP error handling: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.