🎉 RamAPI v1.0 is now available! Read the Getting Started Guide
Documentation
Core Concepts
Error Handling

Error Handling

Robust error handling is essential for production applications. RamAPI provides a comprehensive error handling system with the HTTPError class, custom error handlers, and automatic error formatting.

Table of Contents

  1. HTTPError Class
  2. Throwing Errors
  3. Custom Error Handlers
  4. Error Middleware
  5. Validation Errors
  6. Error Response Format
  7. Error Handling Patterns
  8. Best Practices

HTTPError Class

The HTTPError class extends the standard Error class with HTTP status codes and additional details.

HTTPError Structure

class HTTPError extends Error {
  constructor(
    public statusCode: number,
    message: string,
    public details?: unknown
  )
}

Creating HTTPErrors

import { HTTPError } from 'ramapi';
 
// Basic error
throw new HTTPError(404, 'Resource not found');
 
// With details
throw new HTTPError(400, 'Invalid input', {
  field: 'email',
  reason: 'Email already exists'
});
 
// Common status codes
throw new HTTPError(400, 'Bad Request');
throw new HTTPError(401, 'Unauthorized');
throw new HTTPError(403, 'Forbidden');
throw new HTTPError(404, 'Not Found');
throw new HTTPError(409, 'Conflict');
throw new HTTPError(422, 'Unprocessable Entity');
throw new HTTPError(500, 'Internal Server Error');

Throwing Errors

Throw errors in handlers and middleware to trigger error handling.

In Route Handlers

app.get('/users/:id', async (ctx) => {
  const user = await database.findUser(ctx.params.id);
 
  if (!user) {
    throw new HTTPError(404, 'User not found');
  }
 
  ctx.json({ user });
});

With Details

app.post('/users', async (ctx) => {
  const { email } = ctx.body;
 
  const existing = await database.findByEmail(email);
 
  if (existing) {
    throw new HTTPError(409, 'Email already registered', {
      field: 'email',
      value: email
    });
  }
 
  const user = await database.createUser(ctx.body);
  ctx.json({ user }, 201);
});

In Middleware

const requireAdmin: Middleware = async (ctx, next) => {
  const user = ctx.state.user;
 
  if (!user || user.role !== 'admin') {
    throw new HTTPError(403, 'Admin access required');
  }
 
  await next();
};

Async Errors

app.get('/data', async (ctx) => {
  try {
    const data = await externalAPI.fetch();
    ctx.json({ data });
  } catch (error) {
    // Wrap external errors
    throw new HTTPError(502, 'External service unavailable', {
      originalError: error.message
    });
  }
});

Custom Error Handlers

Define custom error handling logic for your application.

Basic Error Handler

import { createApp, HTTPError } from 'ramapi';
 
const app = createApp({
  onError: async (error, ctx) => {
    console.error('Error:', error);
 
    if (error instanceof HTTPError) {
      ctx.json({
        error: true,
        message: error.message,
        details: error.details
      }, error.statusCode);
    } else {
      ctx.json({
        error: true,
        message: 'Internal server error'
      }, 500);
    }
  }
});

Development vs Production

const isDevelopment = process.env.NODE_ENV === 'development';
 
const app = createApp({
  onError: async (error, ctx) => {
    console.error('Error:', error);
 
    const statusCode = error instanceof HTTPError
      ? error.statusCode
      : 500;
 
    const response: any = {
      error: true,
      message: error.message,
    };
 
    // Include details in HTTPError
    if (error instanceof HTTPError && error.details) {
      response.details = error.details;
    }
 
    // Include stack trace in development
    if (isDevelopment) {
      response.stack = error.stack;
    }
 
    ctx.json(response, statusCode);
  }
});

Logging Errors

const app = createApp({
  onError: async (error, ctx) => {
    // Log to your logging service
    logger.error('Request error', {
      error: error.message,
      stack: error.stack,
      method: ctx.method,
      path: ctx.path,
      statusCode: error instanceof HTTPError ? error.statusCode : 500,
      userId: ctx.state.userId,
    });
 
    // Send response
    if (error instanceof HTTPError) {
      ctx.json({
        error: true,
        message: error.message,
        details: error.details
      }, error.statusCode);
    } else {
      ctx.json({
        error: true,
        message: 'Internal server error'
      }, 500);
    }
  }
});

Error Tracking

import * as Sentry from '@sentry/node';
 
const app = createApp({
  onError: async (error, ctx) => {
    // Report to Sentry
    Sentry.captureException(error, {
      extra: {
        method: ctx.method,
        path: ctx.path,
        params: ctx.params,
        query: ctx.query,
        userId: ctx.state.userId,
      }
    });
 
    // Send response
    const statusCode = error instanceof HTTPError ? error.statusCode : 500;
 
    ctx.json({
      error: true,
      message: error.message,
      ...(error instanceof HTTPError && { details: error.details })
    }, statusCode);
  }
});

Error Middleware

Create middleware specifically for error handling.

Global Error Catcher

const errorCatcher: Middleware = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    console.error('Caught error:', error);
 
    if (error instanceof HTTPError) {
      ctx.json({
        error: true,
        message: error.message,
        details: error.details
      }, error.statusCode);
    } else {
      ctx.json({
        error: true,
        message: 'Internal server error'
      }, 500);
    }
  }
};
 
// Apply early in middleware chain
app.use(errorCatcher);

Error Recovery

const errorRecovery: Middleware = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    // Try to recover
    if (error.code === 'ECONNREFUSED') {
      // Retry logic
      try {
        await next();
        return;
      } catch (retryError) {
        throw new HTTPError(503, 'Service temporarily unavailable');
      }
    }
 
    // Re-throw if can't recover
    throw error;
  }
};

Type-Specific Error Handling

const databaseErrorHandler: Middleware = async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    // Handle database errors
    if (error.code === '23505') { // Unique constraint violation
      throw new HTTPError(409, 'Resource already exists');
    }
 
    if (error.code === '23503') { // Foreign key violation
      throw new HTTPError(400, 'Referenced resource does not exist');
    }
 
    // Re-throw other errors
    throw error;
  }
};

Validation Errors

Validation errors are handled automatically by the validate() middleware.

Validation Error Format

// Request
POST /users
{
  "name": "J",
  "email": "invalid"
}
 
// Response (400 Bad Request)
{
  "error": true,
  "message": "Validation failed",
  "details": {
    "errors": [
      {
        "field": "body.name",
        "message": "String must contain at least 2 character(s)",
        "code": "too_small"
      },
      {
        "field": "body.email",
        "message": "Invalid email",
        "code": "invalid_string"
      }
    ]
  }
}

Custom Validation Error Handling

const app = createApp({
  onError: async (error, ctx) => {
    if (error instanceof HTTPError && error.message === 'Validation failed') {
      // Custom validation error format
      const errors = error.details as { errors: any[] };
 
      ctx.json({
        success: false,
        message: 'Please fix the following errors',
        fields: errors.errors.reduce((acc, err) => {
          const field = err.field.replace('body.', '');
          acc[field] = err.message;
          return acc;
        }, {} as Record<string, string>)
      }, 400);
    } else if (error instanceof HTTPError) {
      ctx.json({
        success: false,
        message: error.message,
        details: error.details
      }, error.statusCode);
    } else {
      ctx.json({
        success: false,
        message: 'Internal server error'
      }, 500);
    }
  }
});

Error Response Format

Standardize error responses across your API.

Standard Format

interface ErrorResponse {
  error: boolean;
  message: string;
  details?: unknown;
  stack?: string; // Development only
}

Consistent Error Responses

const app = createApp({
  onError: async (error, ctx) => {
    const statusCode = error instanceof HTTPError
      ? error.statusCode
      : 500;
 
    const response: ErrorResponse = {
      error: true,
      message: error.message,
    };
 
    if (error instanceof HTTPError && error.details) {
      response.details = error.details;
    }
 
    if (process.env.NODE_ENV === 'development') {
      response.stack = error.stack;
    }
 
    ctx.json(response, statusCode);
  }
});

API-Specific Formats

// REST API format
ctx.json({
  success: false,
  error: {
    code: 'USER_NOT_FOUND',
    message: 'User not found',
    statusCode: 404
  }
}, 404);
 
// JSON:API format
ctx.json({
  errors: [{
    status: '404',
    title: 'Not Found',
    detail: 'User not found',
    source: { pointer: '/users/123' }
  }]
}, 404);

Error Handling Patterns

Common patterns for handling specific errors.

Not Found

app.get('/users/:id', async (ctx) => {
  const user = await database.findUser(ctx.params.id);
 
  if (!user) {
    throw new HTTPError(404, 'User not found', {
      userId: ctx.params.id
    });
  }
 
  ctx.json({ user });
});

Unauthorized

const requireAuth: Middleware = async (ctx, next) => {
  const token = ctx.headers['authorization'];
 
  if (!token) {
    throw new HTTPError(401, 'Authentication required', {
      message: 'Please provide an authorization token'
    });
  }
 
  await next();
};

Forbidden

app.delete('/users/:id', async (ctx) => {
  const currentUser = ctx.state.user;
  const targetUserId = ctx.params.id;
 
  if (currentUser.id !== targetUserId && currentUser.role !== 'admin') {
    throw new HTTPError(403, 'Forbidden', {
      message: 'You do not have permission to delete this user'
    });
  }
 
  await database.deleteUser(targetUserId);
  ctx.json({ message: 'User deleted' });
});

Conflict

app.post('/users', async (ctx) => {
  const { email } = ctx.body;
 
  const existing = await database.findByEmail(email);
 
  if (existing) {
    throw new HTTPError(409, 'Email already exists', {
      field: 'email',
      existingUserId: existing.id
    });
  }
 
  const user = await database.createUser(ctx.body);
  ctx.json({ user }, 201);
});

Rate Limit

const rateLimit: Middleware = async (ctx, next) => {
  const remaining = checkRateLimit(ctx);
 
  if (remaining <= 0) {
    throw new HTTPError(429, 'Too many requests', {
      message: 'Rate limit exceeded. Please try again later.',
      retryAfter: 60
    });
  }
 
  await next();
};

Service Unavailable

app.get('/external-data', async (ctx) => {
  try {
    const data = await externalAPI.fetch();
    ctx.json({ data });
  } catch (error) {
    if (error.code === 'ETIMEDOUT' || error.code === 'ECONNREFUSED') {
      throw new HTTPError(503, 'Service temporarily unavailable', {
        message: 'External service is not responding',
        retryAfter: 30
      });
    }
 
    throw error;
  }
});

Best Practices

1. Use Appropriate Status Codes

// Good - specific status codes
throw new HTTPError(404, 'Not found');
throw new HTTPError(409, 'Conflict');
throw new HTTPError(422, 'Unprocessable entity');
 
// Not recommended - generic 400
throw new HTTPError(400, 'Error');

2. Provide Helpful Error Messages

// Good - actionable message
throw new HTTPError(400, 'Email is required and must be a valid email address');
 
// Not helpful
throw new HTTPError(400, 'Invalid input');

3. Include Relevant Details

// Good
throw new HTTPError(404, 'User not found', {
  userId: ctx.params.id,
  searchedBy: 'id'
});
 
// Minimal
throw new HTTPError(404, 'Not found');

4. Don't Expose Sensitive Information

// Good
throw new HTTPError(401, 'Invalid credentials');
 
// Bad - exposes implementation details
throw new HTTPError(401, 'Password hash mismatch for user@example.com');

5. Log Errors Appropriately

const app = createApp({
  onError: async (error, ctx) => {
    // Log all errors
    logger.error('Request error', {
      error: error.message,
      stack: error.stack,
      path: ctx.path,
    });
 
    // Don't expose internal errors to client
    const isClientError = error instanceof HTTPError && error.statusCode < 500;
 
    ctx.json({
      error: true,
      message: isClientError ? error.message : 'Internal server error'
    }, error instanceof HTTPError ? error.statusCode : 500);
  }
});

6. Handle Async Errors

// Good - errors propagate automatically
app.get('/data', async (ctx) => {
  const data = await fetchData(); // Throws error
  ctx.json({ data });
});
 
// Unnecessary try-catch
app.get('/data', async (ctx) => {
  try {
    const data = await fetchData();
    ctx.json({ data });
  } catch (error) {
    throw error; // Redundant
  }
});

7. Create Error Constants

// errors/constants.ts
export const ERRORS = {
  USER_NOT_FOUND: {
    statusCode: 404,
    message: 'User not found',
    code: 'USER_NOT_FOUND'
  },
  EMAIL_EXISTS: {
    statusCode: 409,
    message: 'Email already registered',
    code: 'EMAIL_EXISTS'
  },
  UNAUTHORIZED: {
    statusCode: 401,
    message: 'Authentication required',
    code: 'UNAUTHORIZED'
  }
} as const;
 
// Usage
import { ERRORS } from './errors/constants.js';
 
throw new HTTPError(
  ERRORS.USER_NOT_FOUND.statusCode,
  ERRORS.USER_NOT_FOUND.message,
  { code: ERRORS.USER_NOT_FOUND.code }
);

8. Test Error Paths

// Test that errors are thrown correctly
describe('GET /users/:id', () => {
  it('should return 404 when user not found', async () => {
    const response = await request(app)
      .get('/users/nonexistent')
      .expect(404);
 
    expect(response.body).toEqual({
      error: true,
      message: 'User not found'
    });
  });
});

Common HTTP Status Codes

CodeMeaningWhen to Use
400Bad RequestInvalid request format
401UnauthorizedAuthentication required or failed
403ForbiddenUser doesn't have permission
404Not FoundResource doesn't exist
409ConflictResource already exists
422Unprocessable EntityValidation failed (semantic errors)
429Too Many RequestsRate limit exceeded
500Internal Server ErrorUnexpected server error
502Bad GatewayUpstream service error
503Service UnavailableTemporary unavailability

Next Steps


Need help? See the Troubleshooting Guide for common issues.