# 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

```typescript
// 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:

```typescript
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

## Related ADRs

- [ADR-034](./0034-repository-pattern-standards.md) - Repository Pattern Standards (extends this ADR)
