Robust error-handling patterns across TypeScript, Python, and Go: typed errors, error boundaries, retries, circuit breakers, and user-facing messages. USE WHEN designing error types, adding retry or circuit-breaker logic, or debugging cascading or swallowed failures.
Robust error-handling patterns across TypeScript, Python, and Go: typed errors, error boundaries, retries, circuit breakers, and user-facing messages. USE WHEN designing error types, adding retry or circuit-breaker logic, or debugging cascading or swallowed failures.
origin
ECC
cluster
quality-eval
version
1.0.0
Error Handling Patterns
Consistent, robust error handling patterns for production applications.
When to Activate
Designing error types or exception hierarchies for a new module or service
Adding retry logic or circuit breakers for unreliable external dependencies
Reviewing API endpoints for missing error handling
Implementing user-facing error messages and feedback
Debugging cascading failures or silent error swallowing
Core Principles
Fail fast and loudly — surface errors at the boundary where they occur; don't bury them
Typed errors over string messages — errors are first-class values with structure
User messages ≠ developer messages — show friendly text to users, log full context server-side
— every block must either handle, re-throw, or log
Never swallow errors silently
catch
Errors are part of your API contract — document every error code a client may receive
TypeScript / JavaScript
Typed Error Classes
// Define an error hierarchy for your domainexportclassAppErrorextendsError {
constructor(message: string,
publicreadonlycode: string,
publicreadonlystatusCode: number = 500,
publicreadonlydetails?: unknown,
) {
super(message)
this.name = this.constructor.name// Maintain correct prototype chain in transpiled ES5 JavaScript.// Required for `instanceof` checks (e.g., `error instanceof NotFoundError`)// to work correctly when extending the built-in Error class.Object.setPrototypeOf(this, new.target.prototype)
}
}
exportclassNotFoundErrorextendsAppError {
constructor(resource: string, id: string) {
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404)
}
}
exportclassValidationErrorextendsAppError {
constructor(message: string, details: { field: string; message: string }[]) {
super(message, 'VALIDATION_ERROR', 422, details)
}
}
exportclassUnauthorizedErrorextendsAppError {
constructor(reason = 'Authentication required') {
super(reason, 'UNAUTHORIZED', 401)
}
}
exportclassRateLimitErrorextendsAppError {
constructor(publicreadonlyretryAfterMs: number) {
super('Rate limit exceeded', 'RATE_LIMITED', 429)
}
}
Result Pattern (no-throw style)
For operations where failure is expected and common (parsing, external calls):
typeResult<T, E = AppError> =
| { ok: true; value: T }
| { ok: false; error: E }
function ok<T>(value: T): Result<T> {
return { ok: true, value }
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error }
}
// UsageasyncfunctionfetchUser(id: string): Promise<Result<User>> {
try {
const user = await db.users.findUnique({ where: { id } })
if (!user) returnerr(newNotFoundError('User', id))
returnok(user)
} catch (e) {
returnerr(newAppError('Database error', 'DB_ERROR'))
}
}
const result = awaitfetchUser('abc-123')
if (!result.ok) {
// TypeScript knows result.error here
logger.error('Failed to fetch user', { error: result.error })
return
}
// TypeScript knows result.value hereconsole.log(result.value.email)
Map error codes to human-readable messages. Keep technical details out of user-visible text.
constUSER_ERROR_MESSAGES: Record<string, string> = {
NOT_FOUND: 'The requested item could not be found.',
UNAUTHORIZED: 'Please sign in to continue.',
FORBIDDEN: "You don't have permission to do that.",
VALIDATION_ERROR: 'Please check your input and try again.',
RATE_LIMITED: 'Too many requests. Please wait a moment and try again.',
INTERNAL_ERROR: 'Something went wrong on our end. Please try again later.',
}
exportfunctiongetUserMessage(code: string): string {
returnUSER_ERROR_MESSAGES[code] ?? USER_ERROR_MESSAGES.INTERNAL_ERROR
}
Error Handling Checklist
Before merging any code that touches error handling:
Every catch block handles, re-throws, or logs — no silent swallowing
API errors follow the standard envelope { error: { code, message } }
User-facing messages contain no stack traces or internal details
Full error context is logged server-side
Custom error classes extend a base AppError with a code field
Async functions surface errors to callers — no fire-and-forget without fallback
Retry logic only retries retriable errors (not 4xx client errors)
React components are wrapped in ErrorBoundary for rendering errors