HSawana9

Unified Error Handling System

Implement a centralized, type-safe error handling system for Next.js applications with structured logging and internationalization support

Overview

This documentation outlines a centralized error handling system designed to solve common pain points in web application development:

Key Problems Solved: - Inconsistent error formats and HTTP status codes across APIs - Difficult debugging due to lack of contextual information - Poor user experience with generic error messages - Type safety issues in error handling

The system introduces a Central Error Dictionary (ErrorMap) and custom AppError class to provide standardized, type-safe, and maintainable error handling workflows.

Core Principles

  • Declarative Definition: All business errors must be predefined in ErrorMap
  • Single Source of Truth: ErrorMap serves as the central metadata repository
  • Structured Logging: Rich contextual metadata for debugging and monitoring
  • User-Friendly: Internationalized error messages with clear UX

Architecture Components

ComponentPurpose
ErrorMapCentral dictionary mapping reason codes to error metadata
Reason CodeUnique uppercase identifier for specific error scenarios
AppError ClassCustom error class extending native Error with enhanced metadata
ErrorMetadataTypeScript interface for structured logging context
Locale FilesJSON files containing user-facing error messages

Implementation Guide

Project Structure Setup

Create the following file structure in your Next.js project:

.
├── types/
   └── errors.ts       # Error-related TypeScript types
├── lib/
   └── errors.ts       # Core error system implementation
└── locales/
    ├── en/
   └── errors.json # English error messages
    └── zh/
        └── errors.json # Chinese error messages

Define Error Metadata Types

Create types/errors.ts with the following interface:

types/errors.ts
export interface ErrorMetadata {
  /** The ID of the user associated with the error */
  userId?: string;

  /** The ID of the specific resource involved */
  resourceId?: string;

  /** The type of the resource involved */
  resourceType?: string;

  /** Unique identifier for request tracing */
  traceId?: string;

  /** The specific action or operation that failed */
  action?: string;

  /** Timestamp when the error occurred */
  createdAt?: string;
}

Implement Core Error System

Create lib/errors.ts with the centralized error handling logic:

lib/errors.ts
import { ErrorMetadata } from "@/types/errors";

// Central Error Dictionary
export const ErrorMap = {
  // 400 Bad Request
  INVALID_INPUT: {
    errorCode: 40001,
    httpStatus: 400,
    category: "BAD_REQUEST",
    message: "The provided input is invalid.",
  },
  // 401 Unauthorized
  TOKEN_EXPIRED: {
    errorCode: 40101,
    httpStatus: 401,
    category: "UNAUTHORIZED",
    message: "Authentication token has expired.",
  },
  INVALID_TOKEN: {
    errorCode: 40102,
    httpStatus: 401,
    category: "UNAUTHORIZED",
    message: "Invalid or tampered authentication token.",
  },
  // 403 Forbidden
  INSUFFICIENT_PERMISSIONS: {
    errorCode: 40301,
    httpStatus: 403,
    category: "FORBIDDEN",
    message: "User lacks required permissions for this action.",
  },
  // 404 Not Found
  RESOURCE_NOT_FOUND: {
    errorCode: 40401,
    httpStatus: 404,
    category: "NOT_FOUND",
    message: "The requested resource could not be found.",
  },
  // 500 Internal Server Error
  DATABASE_CONNECTION_FAILED: {
    errorCode: 50001,
    httpStatus: 500,
    category: "INTERNAL_SERVER_ERROR",
    message: "Failed to establish a connection to the database.",
  },
  // 503 Service Unavailable
  AI_SERVICE_ERROR: {
    errorCode: 50301,
    httpStatus: 503,
    category: "SERVICE_UNAVAILABLE",
    message: "The third-party AI service failed to respond.",
  },
} as const;

export type ReasonCode = keyof typeof ErrorMap;

interface AppErrorOptions {
  metadata?: ErrorMetadata;
  cause?: unknown;
}

// Custom AppError Class
export class AppError extends Error {
  public readonly reason: ReasonCode;
  public readonly errorCode: number;
  public readonly httpStatus: number;
  public readonly category: string;
  public readonly metadata: ErrorMetadata;
  public readonly cause?: unknown;

  constructor(reason: ReasonCode, options: AppErrorOptions = {}) {
    const errorDefinition = ErrorMap[reason];

    super(errorDefinition.message);
    this.name = "AppError";

    // Load static properties from ErrorMap
    this.reason = reason;
    this.errorCode = errorDefinition.errorCode;
    this.httpStatus = errorDefinition.httpStatus;
    this.category = errorDefinition.category;

    // Set dynamic properties
    this.metadata = options.metadata ?? {};
    this.cause = options.cause;
    this.metadata.createdAt = new Date().toISOString();

    // Maintain proper stack trace
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, AppError);
    }
  }
}

Setup Internationalization

Create locale files for user-facing error messages:

locales/en/errors.json
{
  "40001": "Invalid input provided. Please check your data and try again.",
  "40101": "Your session has expired. Please log in again.",
  "40102": "Authentication failed. Please check your credentials.",
  "40301": "You do not have permission to perform this action.",
  "40401": "The resource you are looking for could not be found.",
  "50001": "A server error occurred. Our team has been notified.",
  "50301": "The AI service is temporarily unavailable. Please try again in a few moments.",
  "unknown": "An unexpected error occurred. Please contact support if the problem persists."
}
locales/zh/errors.json
{
  "40001": "输入的数据无效,请检查后重试。",
  "40101": "您的登录已过期,请重新登录。",
  "40102": "身份验证失败,请检查您的凭据。",
  "40301": "您没有权限执行此操作。",
  "40401": "您正在寻找的资源不存在。",
  "50001": "服务器发生错误,我们的团队已收到通知。",
  "50301": "AI 服务暂时不可用,请稍后重试。",
  "unknown": "发生未知错误,如果问题持续存在,请联系支持。"
}

You'll need an i18n library like next-intl to load and use these locale files.

Usage Examples

Backend: Throwing Errors

Use AppError in your API routes or Server Actions with rich metadata:

app/api/projects/[id]/route.ts
import { AppError } from "@/lib/errors";
import { getUserSession } from "@/lib/auth";
import { db } from "@/lib/db";

export async function DELETE(
  request: Request,
  { params }: { params: { id: string } },
) {
  try {
    const session = await getUserSession();
    if (!session) {
      throw new AppError("INVALID_TOKEN");
    }

    const project = await db.project.findUnique({
      where: { id: params.id },
    });

    if (!project) {
      throw new AppError("RESOURCE_NOT_FOUND", {
        metadata: {
          resourceId: params.id,
          resourceType: "Project",
        },
      });
    }

    if (project.ownerId !== session.user.id) {
      throw new AppError("INSUFFICIENT_PERMISSIONS", {
        metadata: {
          userId: session.user.id,
          action: "DELETE",
          resourceId: params.id,
          resourceType: "Project",
        },
      });
    }

    await db.project.delete({ where: { id: params.id } });
    return Response.json({ success: true });
  } catch (error) {
    if (error instanceof AppError) {
      console.error(`AppError: ${error.reason}`, error.metadata);
      return Response.json(
        { error: { reason: error.reason, errorCode: error.errorCode } },
        { status: error.httpStatus },
      );
    }

    console.error("Unknown error:", error);
    return Response.json(
      { error: { reason: "INTERNAL_SERVER_ERROR", errorCode: 50000 } },
      { status: 500 },
    );
  }
}

Frontend: Error Handling

Handle standardized error responses in your frontend with proper internationalization:

components/DeleteProjectButton.tsx
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { useTranslations } from "next-intl";

export function DeleteProjectButton({ projectId }: { projectId: string }) {
  const t = useTranslations("Errors");

  const mutation = useMutation({
    mutationFn: (id: string) =>
      fetch(`/api/projects/${id}`, { method: "DELETE" }),
    onError: async (error: unknown) => {
      try {
        const response = error as Response;
        const data = await response.json();
        const errorCode = data.error?.errorCode;

        if (errorCode) {
          toast.error(t(String(errorCode)));
        } else {
          toast.error(t("unknown"));
        }
      } catch {
        toast.error(t("unknown"));
      }
    },
  });

  return (
    <button onClick={() => mutation.mutate(projectId)}>Delete Project</button>
  );
}

Adding New Error Types

  1. Add to ErrorMap: Define new error with unique code and reason
  2. Update locale files: Add user-facing messages for all supported languages
  3. Extend metadata (optional): Add new fields to ErrorMetadata interface if needed

Best Practices: - Use descriptive reason codes in UPPERCASE_WITH_UNDERSCORES format - Follow HTTP status code conventions for httpStatus values - Include relevant context in metadata for debugging - Never expose sensitive metadata to clients

Benefits

  • Type Safety: Full TypeScript support with compile-time error checking
  • Consistency: Standardized error format across all APIs
  • Debugging: Rich structured logging with contextual metadata
  • Internationalization: Multi-language support for user-facing messages
  • Maintainability: Centralized error definitions reduce code duplication