CentercodePorygon
DittoPorygonAPI

Error Handling

Typed error classes and consistent error responses

SafeReliable

Overview

Use typed error classes from @/lib/errors for consistent error responses. The handleError() wrapper logs errors and returns properly formatted responses.

What it is

A set of error classes that map to HTTP status codes, plus a wrapper function that handles logging and response formatting.

Why we use it

Consistent error responses across all endpoints, automatic logging, and type-safe error creation.

When to use

Any API route or server action that can fail. Throw typed errors, let handleError() format the response.

Key Features

  • Typed error classes for common HTTP errors
  • handleError() wrapper for consistent responses
  • Automatic error logging with context
  • Client-safe error messages (no internal details)

Quick Start

Basic Error Handling

Throwing and handling errors in an API route.

// Using error classes with handleError()
import { handleError, NotFoundError, ValidationError } from '@/lib/errors';

export async function GET(request: Request) {
  try {
    const user = await findUser(id);
    if (!user) {
      throw new NotFoundError('User not found');
    }
    return NextResponse.json({ ok: true, data: user });
  } catch (error) {
    return handleError(error);
  }
}

Patterns

Error Classes

Available error classes and their HTTP status codes.

// Available error classes
import {
  AppError,             // Base class (preferred for custom errors)
  ValidationError,      // 400 Bad Request
  AuthenticationError,  // 401 Unauthorized
  AuthorizationError,   // 403 Forbidden
  NotFoundError,        // 404 Not Found
  ConflictError,        // 409 Conflict
  NotImplementedError,  // 501 Not Implemented
} from '@/lib/errors';

// Usage
throw new ValidationError('Email format is invalid');
throw new NotFoundError('Project not found');
throw new AuthorizationError('Insufficient permissions');
throw new NotImplementedError('SMS channel not yet implemented');

Using handleError()

The standard pattern for catching and returning errors.

// handleError() does three things:
// 1. Logs the error with context
// 2. Returns proper HTTP status code
// 3. Formats consistent error response

export async function POST(request: Request) {
  try {
    // Your logic here
    const result = await someOperation();
    return NextResponse.json({ ok: true, data: result });
  } catch (error) {
    // This handles logging + response formatting
    return handleError(error);
  }
}

// Output for ValidationError:
// Status: 400
// Body: { ok: false, error: { code: 'VALIDATION_ERROR', message: '...' } }

Creating Custom Errors

Extending AppError for domain-specific errors.

// Creating domain-specific errors
import { AppError } from '@/lib/errors';

export class RateLimitError extends AppError {
  constructor(message = 'Too many requests') {
    super(429, { code: 'RATE_LIMIT_EXCEEDED', message });
  }
}

export class PaymentError extends AppError {
  constructor(message: string) {
    super(402, { code: 'PAYMENT_REQUIRED', message });
  }
}

Watch Out

Exposing internal error details to clients

Don't

// Exposing internal details
return NextResponse.json({
  error: error.stack, // Dangerous!
  message: error.message
});

Do

// Using handleError for safe responses
return handleError(error);
// Returns: { ok: false, error: { code, message } }
// Internal details are logged, not exposed

Not logging errors before returning

Don't

// Silent errors
catch (error) {
  return NextResponse.json({ error: 'Something went wrong' });
}

Do

// handleError logs before returning
catch (error) {
  return handleError(error);
  // Logs: { level: 'error', message, stack, context }
}
  • Using wrong HTTP status codes
  • Using generic messages that don't help debugging

Related

Logging

Structured logging patterns

API Architecture

API route patterns

Validation

Input validation