Skip to content

Feature request: Implement Error Handling System for Event Handler #4141

Open
@dreamorosi

Description

@dreamorosi

Use case

As part of the Event Handler implementation (#3251), we need a comprehensive error handling system that provides:

  1. Centralized exception management for consistent error responses across all routes
  2. Pre-built HTTP error classes for common scenarios (400, 401, 404, 500, etc.)
  3. Custom error handler registration allowing developers to handle specific error types
  4. Automatic error response formatting that converts exceptions to proper HTTP responses
  5. Development-friendly error reporting with detailed information in debug mode
  6. Integration with route resolution to catch and handle errors during request processing

This system ensures that errors are handled consistently, securely, and provide good developer experience while maintaining production-ready error responses.

Solution/User Experience

Note

The code snippets below are provided as reference only - they are not exhaustive and final implementation might vary.

// Base error class for all HTTP errors
abstract class ServiceError extends Error {
  abstract readonly statusCode: number;
  abstract readonly errorType: string;
  public readonly details?: Record<string, unknown>;
  
  constructor(
    message: string,
    options?: ErrorOptions,
    details?: Record<string, unknown>
  ) {
    super(message);
    this.name = 'ServiceError';
  }
  
  toJSON(): ErrorResponse {
    return {
      statusCode: this.statusCode,
      error: this.errorType,
      message: this.message,
      ...(this.details && { details: this.details })
    };
  }
}

Other error classes to be implemented, which extend ServiceError above:

  • BadRequestError
  • UnauthorizedError
  • ForbiddenError
  • NotFoundError
  • MethodNotAllowedError
  • InternalServerError
  • ServiceUnavailableError

When implementing, also double check in the Powertools for AWS Lambda (Python) repo if more error classes are present.

Exception Handler Manager

// Centralized exception handling
class ExceptionHandlerManager {
  private handlers: Map<ErrorConstructor, ErrorHandler> = new Map();
  private defaultHandler?: ErrorHandler;
  
  // Register custom error handler for specific error types
  register<T extends Error>(
    errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
    handler: ErrorHandler<T>
  ): void {
    const errorTypes = Array.isArray(errorType) ? errorType : [errorType];
    
    for (const type of errorTypes) {
      this.handlers.set(type, handler as ErrorHandler);
    }
  }
  
  // Set default handler for unhandled errors
  setDefaultHandler(handler: ErrorHandler): void {
    this.defaultHandler = handler;
  }
  
  // Handle an error and return appropriate response
  handle(error: Error, context: ErrorContext): ErrorResponse {
    // 1. Try to find specific handler
    const handler = this.findHandler(error);
    if (handler) {
      try {
        return handler(error, context);
      } catch (handlerError) {
        // Handler itself threw an error - fall back to default
        return this.handleDefault(handlerError, context);
      }
    }
    
    // 2. Handle known ServiceError types
    if (error instanceof ServiceError) {
      return this.handleServiceError(error, context);
    }
    
    // 3. Use default handler or built-in fallback
    return this.handleDefault(error, context);
  }
  
  private findHandler(error: Error): ErrorHandler | null {
    // Try exact type match first
    const exactHandler = this.handlers.get(error.constructor as ErrorConstructor);
    if (exactHandler) return exactHandler;
    
    // Try instanceof checks for inheritance
    for (const [errorType, handler] of this.handlers) {
      if (error instanceof errorType) {
        return handler;
      }
    }
    
    // Try name-based matching (for cases where instanceof fails due to bundling)
    for (const [errorType, handler] of this.handlers) {
      if (error.name === errorType.name) {
        return handler;
      }
    }
    
    return null;
  }
  
  private handleServiceError(error: ServiceError, context: ErrorContext): ErrorResponse {
    return {
      statusCode: error.statusCode,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(error.toJSON())
    };
  }
  
  private handleDefault(error: Error, context: ErrorContext): ErrorResponse {
    if (this.defaultHandler) {
      return this.defaultHandler(error, context);
    }
    
    // Built-in fallback for unhandled errors
    const isProduction = process.env.NODE_ENV === 'production';
    const isDevelopment = process.env.POWERTOOLS_DEV === 'true' || 
                         process.env.POWERTOOLS_EVENT_HANDLER_DEBUG === 'true';
    
    return {
      statusCode: 500,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        statusCode: 500,
        error: 'Internal Server Error',
        message: isProduction ? 'Internal Server Error' : error.message,
        ...(isDevelopment && { 
          stack: error.stack,
          details: { errorName: error.name }
        })
      })
    };
  }
}

Error Handler Registration API (RFC #3500 DX)

// Integration with BaseRouter following RFC #3500 DX patterns
abstract class BaseRouter {
  protected exceptionManager = new ExceptionHandlerManager();
  
  // Primary error handler registration (matches RFC #3500)
  errorHandler<T extends Error>(
    errorType: ErrorConstructor<T> | ErrorConstructor<T>[],
    handler: ErrorHandler<T>
  ): void {
    this.exceptionManager.register(errorType, handler);
  }
  
  // Specialized handlers (matches RFC #3500)
  notFound(handler: ErrorHandler<NotFoundError>): void {
    this.exceptionManager.register(NotFoundError, handler);
  }
  
  methodNotAllowed(handler: ErrorHandler<MethodNotAllowedError>): void {
    this.exceptionManager.register(MethodNotAllowedError, handler);
  }
  
  // Internal method to handle errors during route processing
  protected handleError(error: Error, context: RequestContext): ErrorResponse {
    const errorContext: ErrorContext = {
      path: context.path,
      method: context.method,
      headers: context.headers,
      timestamp: new Date().toISOString(),
      requestId: context.requestId
    };
    
    return this.exceptionManager.handle(error, errorContext);
  }
}

Developer Experience (Following RFC #3500 Patterns)

// Manual usage pattern (from RFC #3500)
const app = new APIGatewayRestResolver();

// Register error handlers using app.errorHandler()
app.errorHandler(ZodError, (error) => {
  return {
    statusCode: 400,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      statusCode: 400,
      error: 'Validation Error',
      message: 'Request validation failed',
      details: error.issues
    })
  };
});

// Handle multiple error types with single handler
app.errorHandler([TimeoutError, ConnectionError], (error) => {
  return {
    statusCode: 503,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      statusCode: 503,
      error: 'Service Unavailable',
      message: 'External service temporarily unavailable'
    })
  };
});

// Specialized handlers
app.notFound((error) => {
  return {
    statusCode: 404,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      statusCode: 404,
      error: 'Not Found',
      message: `Route not found`
    })
  };
});

app.methodNotAllowed((error) => {
  return {
    statusCode: 405,
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      statusCode: 405,
      error: 'Method Not Allowed',
      message: 'HTTP method not supported for this route'
    })
  };
});

// Class method decorator usage pattern (from RFC #3500)
class Lambda {
  @app.errorHandler(ZodError)
  public async handleValidationError(error: ZodError) {
    logger.error('Request validation failed', {
      path: app.currentEvent.path,
      error: error.issues
    });
    
    return {
      statusCode: 400,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        statusCode: 400,
        error: 'Validation Error',
        message: 'Invalid request parameters'
      })
    };
  }

  @app.notFound()
  public async handleNotFound(error: NotFoundError) {
    logger.info('Route not found', { 
      path: app.currentEvent.path,
      method: app.currentEvent.method 
    });
    
    return {
      statusCode: 404,
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        statusCode: 404,
        error: 'Not Found',
        message: 'The requested resource was not found'
      })
    };
  }

  @app.get('/users/:id')
  public async getUserById({ id }) {
    if (!id || id === '0') {
      throw new BadRequestError('User ID must be a positive number');
    }
    
    const user = await getUserById(id);
    if (!user) {
      throw new NotFoundError(`User with ID ${id} not found`);
    }
    
    return user;
  }
}

export const handler = async (event, context) => app.resolve(event, context);

Pre-built Error Usage (RFC #3500 Style)

// Route handlers can throw pre-built errors directly (from RFC #3500)
app.get('/bad-request-error', () => {
  throw new BadRequestError('Missing required parameter'); // HTTP 400
});

app.get('/unauthorized-error', () => {
  throw new UnauthorizedError('Unauthorized'); // HTTP 401
});

app.get('/not-found-error', () => {
  throw new NotFoundError(); // HTTP 404
});

app.get('/internal-server-error', () => {
  throw new InternalServerError('Internal server error'); // HTTP 500
});

app.get('/service-error', () => {
  throw new ServiceUnavailableError('Something went wrong!'); // HTTP 503
});

Error Response Formatting

// Consistent error response structure
interface ErrorResponse {
  statusCode: number;
  headers: Record<string, string>;
  body: string;
}

interface ErrorContext {
  path: string;
  method: string;
  headers: Record<string, string>;
  timestamp: string;
  requestId?: string;
}

interface ErrorHandler<T extends Error = Error> {
  (error: T, context?: ErrorContext): ErrorResponse;
}

interface ErrorConstructor<T extends Error = Error> {
  new (...args: any[]): T;
  prototype: T;
}

// Error response body structure
interface ErrorResponseBody {
  statusCode: number;
  error: string;
  message: string;
  details?: Record<string, any>;
  timestamp?: string;
  path?: string;
  requestId?: string;
}

Integration with Route Resolution

// Integration point with route resolution system
abstract class BaseRouter {
  async resolve(event: any, context: any): Promise<any> {
    try {
      const requestContext = this.createRequestContext(event, context);
      
      // Route matching (from Route Matching System)
      const resolvedRoute = this.resolveRoute(requestContext.method, requestContext.path);
      
      if (!resolvedRoute) {
        // No route found - throw NotFoundError
        throw new NotFoundError(`Route ${requestContext.method} ${requestContext.path} not found`);
      }
      
      // Execute route handler
      const result = await resolvedRoute.route.handler(resolvedRoute.params, requestContext);
      
      // Build response (from Response Handling System)
      return this.buildResponse(result, resolvedRoute.route);
      
    } catch (error) {
      // All errors are handled centrally
      const errorResponse = this.handleError(error as Error, requestContext);
      return this.convertErrorResponseToLambdaFormat(errorResponse);
    }
  }
  
  private convertErrorResponseToLambdaFormat(errorResponse: ErrorResponse): any {
    return {
      statusCode: errorResponse.statusCode,
      headers: errorResponse.headers,
      body: errorResponse.body,
      isBase64Encoded: false
    };
  }
}

Implementation Details

Scope - In Scope:

  • ServiceError hierarchy: Base class and common HTTP error classes
  • ExceptionHandlerManager: Centralized error handling with custom handler registration
  • RFC RFC: Event Handler for REST APIs #3500 compliant API: errorHandler(), notFound(), methodNotAllowed() methods
  • Class method decorator support: @app.errorHandler(ErrorClass) pattern
  • Pre-built error classes: BadRequestError, UnauthorizedError, NotFoundError, etc.
  • Automatic error response formatting: Consistent JSON error responses
  • Integration with route resolution: Error handling during request processing

Integration Points:

  • BaseRouter: Provides error handler registration methods following RFC RFC: Event Handler for REST APIs #3500
  • Route Resolution: Catches and handles errors during request processing
  • Response Handling: Converts error responses to Lambda proxy format

Data Flow:

Route Handler throws Error
    ↓
BaseRouter.resolve() catches error
    ↓
ExceptionHandlerManager.handle()
    ↓
Find registered handler or use default
    ↓
Format error response
    ↓
Convert to Lambda proxy format

Alternative solutions

Acknowledgment

Future readers

Please react with 👍 and your use case to help us understand customer demand.

Metadata

Metadata

Assignees

No one assigned

    Labels

    event-handlerThis item relates to the Event Handler Utilityfeature-requestThis item refers to a feature request for an existing or new utilityon-holdThis item is on-hold and will be revisited in the future

    Type

    No type

    Projects

    Status

    On hold

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions