| name | API Error Handling |
| description | ENFORCE centralized, secure error handling. Catch ALL errors in one place. Log everything. Return safe, consistent responses. Prevent stack trace leaks, unhandled promise rejections, inconsistent error formats, and silent failures. Trigger: "handle errors", "build an error handler", "fix the crash", "return an error response".
|
| category | backend |
| version | 3.0.0 |
| last_updated | 2026-06-28T00:00:00.000Z |
| stacks | ["Express","FastAPI","Next.js 16 (Route Handlers)","Nuxt","Django"] |
| related_skills | ["api-route-structure","backend-validation-layers"] |
Production API Error Handling
IDENTIFY: When to Activate
Activate whenever writing:
- API route logic that could fail
- Database queries or service calls
- Third-party API integrations
- Form submissions or mutations
CORE PRINCIPLE
Catch ALL errors in one place. Log everything. Return safe, consistent responses.
Two error types with different handling:
| Type | Source | HTTP Status | Client Message | Log Level |
|---|
| Operational (expected) | "Not found", "Invalid email" | 400-404 | The specific error message | INFO/WARN |
| Programmer (unexpected) | Null pointer, DB fail, timeout | 500 | "Internal Server Error" | ERROR/ALERT |
EXECUTE: Instructions
Step 1: Create Custom Error Class with Status Code
export class AppError extends Error {
public readonly statusCode: number;
public readonly isOperational: boolean;
public readonly code?: string;
constructor(message: string, statusCode: number, options?: { code?: string; isOperational?: boolean }) {
super(message);
this.statusCode = statusCode;
this.isOperational = options?.isOperational ?? true;
this.code = options?.code;
Error.captureStackTrace(this, this.constructor);
}
}
export const Errors = {
notFound: (resource: string) => new (, , { : }),
: (message, , { : }),
: (message, , { : }),
: (message, , { : }),
: (message, , { : }),
: (message, , { : }),
};
Step 2: Throw Errors from Business Logic: NOT Route Handlers
async function getUserById(id: string) {
const user = await db.user.findUnique({ where: { id } });
if (!user) throw Errors.notFound('User');
return user;
}
app.get('/api/users/:id', async (req, res, next) => {
try {
const user = await getUserById(req.params.id);
res.json({ data: user });
} catch (err) {
next(err);
}
});
Step 3: Build Global Error Handler (ONE handler: used by ALL routes)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(`[${new Date().toISOString()}] ${err.message}`, {
stack: err.stack, path: req.path, method: req.method,
});
if (err instanceof AppError) {
return res.status(err.statusCode).json({
error: { message: err.message, status: err.statusCode },
});
}
const isProduction = process.env.NODE_ENV === 'production';
return res.status(500).json({
error: {
: isProduction ? : err.,
: ,
},
});
});
Step 4: Standardize Error Response Format (RFC 9457 Problem Details)
ALWAYS use this structure:
{
"error": { "message": "User not found", "status": 404 }
}
{
"type": "https://api.example.com/errors/not-found",
"title": "Resource Not Found",
"status": 404,
"detail": "The user with ID 'abc123' does not exist.",
"instance": "/api/users/abc123"
}
Step 5: Map Database Errors to User-Friendly Messages
NEVER return raw database errors:
try {
await db.user.create({ data });
} catch (err) {
if (err instanceof Prisma.PrismaClientKnownRequestError) {
if (err.code === 'P2002') throw Errors.conflict('A user with this email already exists');
if (err.code === 'P2025') throw Errors.notFound('Related record');
}
throw err;
}
Stack-Specific Patterns
Express 5 (current stable: catches async errors natively)
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
});
Express 4 (legacy: requires express-async-errors)
import 'express-async-errors';
Next.js 16 Route Handlers: withErrorHandler wrapper
import { NextResponse } from 'next/server';
type Handler = (...args: any[]) => Promise<NextResponse>;
export function withErrorHandler(handler: Handler) {
return async (...args: any[]) => {
try {
return await handler(...args);
} catch (err) {
console.error(err);
if (err instanceof AppError) {
return NextResponse.json(
{ error: { message: err.message, status: err.statusCode } },
{ status: err.statusCode }
);
}
const isProduction = process.env.NODE_ENV === 'production';
return NextResponse.json(
{ error: { : isProduction ? : (err )., : } },
{ : }
);
}
};
}
= ( (: ) => {
user = (request...()!);
.({ : user });
});
FastAPI
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class AppError(Exception):
def __init__(self, message: str, status_code: int):
self.message = message
self.status_code = status_code
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(
status_code=exc.status_code,
content={"error": {"message": exc.message, "status": exc.status_code}}
)
VALIDATE: Quality Gates
ANTI-PATTERNS: ALWAYS Avoid
| Anti-Pattern | Why Wrong | Fix |
|---|
catch (e) { /* empty */ } | Silent failure, undiscoverable bugs | At minimum log the error |
res.status(400).json({ error: 'DB failed' }) | Wrong status code (500, not 400) | Database failures are server errors |
res.status(200).json({ error: 'Not found' }) | Success status with error body | Return 404 |
| Inline error handling in every route | Inconsistent, duplicated code | Centralize in global handler |
Returning err.stack to client | Leaks server paths, secrets | Strip in production |
OUTPUT: What This Skill Produces
interface ErrorHandlingSetup {
errorClass: string;
errorFactory: Array<{ name: string; status: number; code: string }>;
globalHandler: string;
rfc9457: boolean;
loggingTarget: 'console' | 'sentry' | 'datadog' | 'axiom';
dbErrorMapping: Array<{ driver: string; codes: Array<{ dbCode: string; httpStatus: number; message: string }> }>;
}