Files
atelier/examples/good/error-handler.md
T
Jon Chery 496303471d docs(milestone): complete v0.1 — initial framework
---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.
2026-08-05 00:36:55 +00:00

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 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).