Files
flyer-crawler.projectium.com/docs/adr/0001-standardized-error-handling.md

6.9 KiB

ADR-001: Standardized Error Handling for Service and Repository Layers

Date: 2025-12-12

Status: Accepted

Implemented: 2026-01-07

Context

Our application has experienced a recurring pattern of bugs and brittle tests related to error handling, specifically for "resource not found" scenarios. The root causes identified are:

  1. Inconsistent Return Types: Database repository methods that fetch a single entity (e.g., getUserById, getRecipeById) had inconsistent behavior when an entity was not found. Some returned undefined, some returned null, and others threw a generic Error.
  2. Burden on Callers: This inconsistency forced route handlers (the callers) to implement defensive checks for undefined or null before sending a response. These checks were often forgotten or implemented incorrectly.
  3. Incorrect HTTP Status Codes: When a route handler forgot to check for an undefined result and passed it to res.json(), the Express framework would interpret this as a server-side failure, resulting in an incorrect 500 Internal Server Error instead of the correct 404 Not Found.
  4. Brittle Tests: Unit and integration tests for routes were unreliable. Mocks often threw a generic new Error() when the actual implementation returned undefined or a specific custom error, leading to unexpected 500 status codes in test environments.

This pattern led to increased development friction, difficult-to-diagnose bugs, and a fragile test suite.

Decision

We will adopt a strict, consistent error-handling contract for the service and repository layers.

  1. Always Throw on Not Found: Any function or method responsible for fetching a single, specific resource (e.g., by ID, checksum, or other unique identifier) MUST throw a NotFoundError if that resource does not exist. It MUST NOT return null or undefined to signify absence.

  2. Use Specific, Custom Errors: For other known, predictable failure modes (e.g., unique constraint violations, foreign key violations), the repository layer MUST throw the corresponding custom DatabaseError subclass (e.g., UniqueConstraintError, ForeignKeyConstraintError).

  3. Centralize HTTP Status Mapping: The errorHandler middleware is the single source of truth for mapping these specific error types to their corresponding HTTP status codes (e.g., NotFoundError -> 404, UniqueConstraintError -> 409).

  4. Simplify Route Handlers: Route handlers should be simplified to use a standard try...catch block. All errors caught from the service/repository layer should be passed directly to next(error), relying on the errorHandler middleware to format the final response. No special if (result === undefined) checks are needed.

Consequences

Positive

Robustness: Eliminates an entire class of bugs where undefined is passed to res.json(), preventing incorrect 500 errors. Consistency & Predictability: All data-fetching methods now have a predictable contract. They either return the expected data or throw a specific, typed error. Developer Experience: Route handlers become simpler, cleaner, and easier to write correctly. The cognitive load on developers is reduced as they no longer need to remember to check for undefined. Improved Testability: Tests become more reliable and realistic. Mocks can now throw the exact error type (new NotFoundError()) that the real implementation would, ensuring tests accurately reflect the application's behavior. Centralized Control: Error-to-HTTP-status logic is centralized in the errorHandler middleware, making it easy to manage and modify error responses globally.

Negative

Initial Refactoring: Requires a one-time effort to audit and refactor all existing repository methods to conform to this new standard. Convention Adherence: Developers must be aware of and adhere to this convention. This ADR serves as the primary documentation for this pattern.

Implementation Details

Custom Error Types

All custom errors are defined in src/services/db/errors.db.ts:

Error Class HTTP Status PostgreSQL Code Use Case
NotFoundError 404 - Resource not found
UniqueConstraintError 409 23505 Duplicate key violation
ForeignKeyConstraintError 400 23503 Referenced record doesn't exist
NotNullConstraintError 400 23502 Required field is null
CheckConstraintError 400 23514 Check constraint violated
InvalidTextRepresentationError 400 22P02 Invalid data type format
NumericValueOutOfRangeError 400 22003 Numeric overflow
ValidationError 400 - Request validation failed
ForbiddenError 403 - Access denied

Error Handler Middleware

The centralized error handler in src/middleware/errorHandler.ts:

  1. Catches all errors from route handlers
  2. Maps custom error types to HTTP status codes
  3. Logs errors with appropriate severity (warn for 4xx, error for 5xx)
  4. Returns consistent JSON error responses
  5. Includes error ID for server errors (for support correlation)

Usage Pattern

// In repository (throws NotFoundError)
async function getUserById(id: number): Promise<User> {
  const result = await pool.query('SELECT * FROM users WHERE id = $1', [id]);
  if (result.rows.length === 0) {
    throw new NotFoundError(`User with ID ${id} not found.`);
  }
  return result.rows[0];
}

// In route handler (simple try/catch)
router.get('/:id', async (req, res, next) => {
  try {
    const user = await getUserById(req.params.id);
    res.json(user);
  } catch (error) {
    next(error); // errorHandler maps NotFoundError to 404
  }
});

Centralized Error Handler Helper

The handleDbError function in src/services/db/errors.db.ts provides centralized PostgreSQL error handling:

import { handleDbError } from './errors.db';

try {
  await pool.query('INSERT INTO users (email) VALUES ($1)', [email]);
} catch (error) {
  handleDbError(
    error,
    logger,
    'Failed to create user',
    { email },
    {
      uniqueMessage: 'A user with this email already exists.',
      defaultMessage: 'Failed to create user.',
    },
  );
}

Key Files

  • src/services/db/errors.db.ts - Custom error classes and handleDbError utility
  • src/middleware/errorHandler.ts - Centralized Express error handling middleware
  • ADR-034 - Repository Pattern Standards (extends this ADR)