496303471d
---ci--- project: atelier phase: 7 milestone: v0.1 status: complete phase_role: final milestone_complete: true requirements: covered: [ATELIER-01, ATELIER-02, ATELIER-03, ATELIER-04, ATELIER-05, ATELIER-06, ATELIER-07, ATELIER-08, ATELIER-09, ATELIER-10, ATELIER-11, ATELIER-12, ATELIER-13, ATELIER-14, ATELIER-15, ATELIER-16, ATELIER-17, ATELIER-18, ATELIER-19, ATELIER-20, ATELIER-21, ATELIER-22, ATELIER-23, ATELIER-24, ATELIER-25, ATELIER-26, ATELIER-27, ATELIER-28, ATELIER-29, ATELIER-30, ATELIER-31, ATELIER-32, ATELIER-33, ATELIER-34, ATELIER-35] partial: [] ship: milestone: v0.1 type: NFR tag: v0.0.7 merge: milestone/v0.1-atelier -> main release: https://git.cloudinit.dev/cloudinit-bot/atelier/releases/tag/v0.0.7 ---/ci--- Milestone v0.1 — Initial Framework (NFR, complete). 8 core principles (C1-C8), 11 domains, 110 domain principles, 27 derived docs, 4 good + 3 bad examples, 4 language docs, full matrix, 3 review docs. All 35 requirements covered. 7 patches (v0.0.0 pre-execution through v0.0.7 final). v0.0.7 IS the v0.1.0 milestone release.
139 lines
5.3 KiB
Markdown
139 lines
5.3 KiB
Markdown
# 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<string, unknown>;
|
|
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). |