Overview

Centralized error handling for your Fastify app. Includes custom error classes for common HTTP errors.

Features

  • Global error catching
  • Custom error classes
  • Environment-aware responses
  • Request ID inclusion
  • Not found handler

Installation

npx hanma add fastify-error-handler --framework fastify

Dependencies

fastify

Code

import { FastifyInstance, FastifyRequest, FastifyReply, FastifyError } from "fastify";

/**
 * Custom application error class
 */
export class AppError extends Error {
  public readonly statusCode: number;
  public readonly code: string;
  public readonly isOperational: boolean;
  public readonly details?: unknown;

  constructor(
    message: string,
    statusCode = 500,
    code = "INTERNAL_ERROR",
    details?: unknown
  ) {
    super(message);
    this.name = "AppError";
    this.statusCode = statusCode;
    this.code = code;
    this.isOperational = true;
    this.details = details;
    Error.captureStackTrace(this, this.constructor);
  }
}

/**
 * Common error types
 */
export class NotFoundError extends AppError {
  constructor(resource = "Resource", details?: unknown) {
    super(`${resource} not found`, 404, "NOT_FOUND", details);
  }
}

export class ValidationError extends AppError {
  constructor(message = "Validation failed", details?: unknown) {
    super(message, 400, "VALIDATION_ERROR", details);
  }
}

export class UnauthorizedError extends AppError {
  constructor(message = "Authentication required", details?: unknown) {
    super(message, 401, "UNAUTHORIZED", details);
  }
}

export class ForbiddenError extends AppError {
  constructor(message = "Access denied", details?: unknown) {
    super(message, 403, "FORBIDDEN", details);
  }
}

export class ConflictError extends AppError {
  constructor(message = "Resource conflict", details?: unknown) {
    super(message, 409, "CONFLICT", details);
  }
}

export class RateLimitError extends AppError {
  constructor(retryAfter?: number) {
    super("Rate limit exceeded", 429, "RATE_LIMIT_EXCEEDED", { retryAfter });
  }
}

/**
 * Error response format
 */
interface ErrorResponse {
  success: false;
  error: {
    code: string;
    message: string;
    details?: unknown;
    stack?: string;
  };
  requestId?: string;
}

/**
 * Register global error handler
 */
export function registerErrorHandler(app: FastifyInstance): void {
  const isDev = process.env.NODE_ENV === "development";

  app.setErrorHandler((error: FastifyError, request: FastifyRequest, reply: FastifyReply) => {
    // Log the error
    if (error instanceof AppError && error.isOperational) {
      request.log.warn({ error, requestId: request.id }, error.message);
    } else {
      request.log.error({ error, requestId: request.id }, "Unexpected error");
    }

    // Build response
    const response: ErrorResponse = {
      success: false,
      error: {
        code: (error as AppError).code ?? "INTERNAL_ERROR",
        message: error.message,
        details: (error as AppError).details,
        stack: isDev ? error.stack : undefined,
      },
      requestId: request.id,
    };

    // Determine status code
    const statusCode = 
      (error as AppError).statusCode ?? 
      error.statusCode ?? 
      500;

    // Handle specific Fastify errors
    if (error.validation) {
      response.error.code = "VALIDATION_ERROR";
      response.error.details = error.validation;
      reply.status(400).send(response);
      return;
    }

    // Don't expose internal error details in production
    if (statusCode >= 500 && !isDev) {
      response.error.message = "Internal server error";
      delete response.error.details;
    }

    reply.status(statusCode).send(response);
  });

  // 404 handler
  app.setNotFoundHandler((request: FastifyRequest, reply: FastifyReply) => {
    reply.status(404).send({
      success: false,
      error: {
        code: "NOT_FOUND",
        message: "Route not found",
        details: {
          method: request.method,
          path: request.url,
        },
      },
      requestId: request.id,
    });
  });
}

// Usage:
// registerErrorHandler(app);
// throw new NotFoundError("User");
// throw new ValidationError("Email is required", { field: "email" });

Usage

import { registerErrorHandler, NotFoundError, ValidationError } from './middleware/error-handler';

registerErrorHandler(app);

// Throw custom errors
throw new NotFoundError('User');
throw new ValidationError('Email is required', { field: 'email' });

Dependencies

Frequently Asked Questions

How do I install Error Handler?
Run the following command: npx hanma add fastify-error-handler --framework fastify
What dependencies are required?
This snippet requires: fastify. Make sure to install them before using the snippet.
How do I use this snippet?
Copy the code and paste it into your project. The snippet will be saved to src/middleware/error-handler.ts.
Can I customize this snippet?
Yes! All snippets are copy-paste code that you own. Feel free to modify them to fit your needs.
Does this work with other frameworks?
This snippet is designed for the specific framework. Check the other framework pages for equivalent implementations.