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
Component | Purpose |
---|---|
ErrorMap | Central dictionary mapping reason codes to error metadata |
Reason Code | Unique uppercase identifier for specific error scenarios |
AppError Class | Custom error class extending native Error with enhanced metadata |
ErrorMetadata | TypeScript interface for structured logging context |
Locale Files | JSON 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:
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:
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:
{
"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."
}
{
"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:
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:
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
- Add to ErrorMap: Define new error with unique code and reason
- Update locale files: Add user-facing messages for all supported languages
- 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