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.
5.3 KiB
5.3 KiB
Good Example: Error Handler
An error handler that follows Atelier's Errors principles. Each aspect cites the principle it satisfies.
The Handler
// 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
AppErrortype. They are values, not raw exceptions. - The
codeis 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 withcode: err.code(specific, e.g.,INVALID_EMAIL).NotFoundError→ 404 withcode: 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_idties 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)
messageis for the developer (log + response). The client UI renders a user-facing message based oncode.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_ERRORis 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_ERRORwith 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_idon 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).