# Good Example: Error Handler > An error handler that follows Atelier's Errors principles. Each aspect cites the principle it satisfies. ## The Handler ```typescript // Centralized error middleware import { v4 as uuidv4 } from 'uuid'; type AppError = { code: string; message: string; statusCode: number; details?: Record; cause?: unknown; }; function errorHandler(err: unknown, req: Request, res: Response, next: NextFunction) { const requestId = req.headers['x-request-id'] as string || uuidv4(); const appError = normalizeError(err, requestId); // Log with full context (server-side only) logger.error({ request_id: requestId, code: appError.code, message: appError.message, details: appError.details, path: req.path, method: req.method, user_id: req.user?.id, stack: err instanceof Error ? err.stack : undefined, }); // Respond with safe, structured error res.status(appError.statusCode).json({ error: { code: appError.code, message: appError.message, request_id: requestId, ...(appError.details ? { details: appError.details } : {}), }, }); } function normalizeError(err: unknown, requestId: string): AppError { // Known error types — preserve their code and status if (err instanceof ValidationError) { return { code: err.code, message: err.message, statusCode: 422, details: err.details, }; } if (err instanceof NotFoundError) { return { code: err.code, message: err.message, statusCode: 404 }; } if (err instanceof AuthError) { return { code: 'AUTH_REQUIRED', message: 'Authentication required', statusCode: 401 }; } // Unknown error — fail securely, do not leak internals return { code: 'INTERNAL_ERROR', message: 'An internal error occurred', statusCode: 500, cause: err, }; } ``` ## What Makes It Good ### Errors are Data (Errors P1) - Errors are normalized into a structured `AppError` type. They are values, not raw exceptions. - The `code` is a stable, machine-consumable string (`VALIDATION_FAILED`, not a free message). - The handler treats every error as data to be transformed, not a flow to be caught. ### Fail Loudly (Errors P2) - No `catch (e) {}`. Every error reaches the handler. - No silent swallowing. The error is logged and responded to. - The middleware is centralized — every route uses it. No route can "forget" to handle errors. ### Fail Specifically (Errors P3) - `ValidationError` → 422 with `code: err.code` (specific, e.g., `INVALID_EMAIL`). - `NotFoundError` → 404 with `code: err.code` (e.g., `USER_NOT_FOUND`). - Unknown → 500 with `INTERNAL_ERROR` (generic only when truly unknown). - Never "Something went wrong" — always a specific code. ### Preserve Context (Errors P4) - Server-side log includes: `request_id`, `code`, `message`, `details`, `path`, `method`, `user_id`, `stack`. - Client-side response includes: `code`, `message`, `request_id`, `details`. - The `request_id` ties the client response to the server log. Debuggable. ### Recoverable When Possible (Errors P5) - 422 (validation) — the client can fix and retry. - 401 (auth) — the client can re-auth and retry. - 404 (not found) — the client can correct the ID. - Only 500 is "the server has a bug" — not recoverable by the client. ### Unrecoverable Means Stop (Errors P6) - A 500 does not limp on. It logs and returns. - The server does not try to "recover" from an unknown error by guessing. It fails fast. ### Errors are Boundaries (Errors P7) - The middleware is the boundary. Internal error types (`ValidationError`, `NotFoundError`) are translated to HTTP responses here. - Internal code throws domain errors; the boundary translates to HTTP. No internal error type leaks to the client. ### User-Facing Errors are UX (Errors P8) - `message` is for the developer (log + response). The client UI renders a user-facing message based on `code`. - `INTERNAL_ERROR` → "Something went wrong. We're on it." (user-facing), not the stack. ### Errors are Logged (Errors P9) - Every error is logged at ERROR level with full context. - The handling is the recovery (return a response); the log is the memory (investigate later). ### Errors Don't Lie (Errors P10) - The status code matches the error type. 422 for validation, not 200. - `INTERNAL_ERROR` is returned only for unknown errors. Known errors get their specific code. - Never claim success on failure. Never claim failure on success. ### Security (Security P5 Output Safety, P9 Secret Hygiene) - Unknown errors return `INTERNAL_ERROR` with no internal details. No stack trace to the client. - The stack is logged server-side, never sent to the client. - The error message does not echo the input (which may contain a token). ### Observability (Observability P2 Correlation, P3 Context) - `request_id` on every error. Correlatable across services. - Sufficient context in the log: path, method, user_id. "What was the user doing?" is answerable. ## What This Example Does NOT Do (And Why That's Good) - Does not `catch (e) { return null }` — silent failure (P2). - Does not return 200 with `{ error: ... }` — the status code lies (API P9). - Does not send the stack trace to the client — information leak (Security P5). - Does not log the request body — may contain secrets (Security P9, Observability P6). - Does not use a generic `Error("error")` — not specific (P3).