Error Handling#
Centralized error handling with proper logging, status codes, and development/production modes.
Global Error Handler#
import { DrizzleQueryError } from "drizzle-orm/errors";
import type { NextFunction, Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import config from "../config.js";
import logger from "../utils/logger.js";
import { commonResps, getClientIp } from "../utils/utils.js";
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
// Log to console in dev mode for easy debugging
if (config.devMode) {
console.error(err);
}
// Log to winston for production monitoring
logger.error(err.message, {
endpoint: `${req.method} ${req.originalUrl}`,
requestor: { ip: getClientIp(req), userAgent: req.get("User-Agent") },
err,
});
let status = StatusCodes.INTERNAL_SERVER_ERROR;
let resp: Record<string, any> = commonResps.internalError;
// Handle database constraint violations
if (err instanceof DrizzleQueryError) {
if ((err as any).cause?.code === "23505") {
status = StatusCodes.CONFLICT;
resp = {
error: "Database level resource conflict",
code: "resource_conflict",
};
}
}
// Include stack trace in development
if (config.devMode) {
resp.stack = err.stack;
}
res.status(status).json(resp);
}
404 Handler#
import type { Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
export function handler404(req: Request, res: Response) {
res.status(StatusCodes.NOT_FOUND).json({
error: "Endpoint not found",
code: "not_found",
endpoint: `${req.method} ${req.originalUrl}`,
});
}
Register Error Handlers#
// ⚠️ Must be registered LAST (after all routes)
app.use(handler404);
app.use(errorHandler);
Status Codes Reference#
import { StatusCodes } from "http-status-codes";
// Success
StatusCodes.OK // 200 - Success
StatusCodes.CREATED // 201 - Resource created
// Client Errors
StatusCodes.BAD_REQUEST // 400 - Invalid request
StatusCodes.UNAUTHORIZED // 401 - Not authenticated
StatusCodes.FORBIDDEN // 403 - Authenticated but no permission
StatusCodes.NOT_FOUND // 404 - Resource not found
StatusCodes.CONFLICT // 409 - Resource already exists
// Server Errors
StatusCodes.INTERNAL_SERVER_ERROR // 500 - Server error
Usage in Routes#
import { StatusCodes } from "http-status-codes";
import type { Request, Response, NextFunction } from "express";
router.get("/:id", async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
return res.status(StatusCodes.NOT_FOUND).json({
error: "User not found",
code: "user_not_found",
});
}
res.status(StatusCodes.OK).json({ user });
} catch (err) {
next(err); // Pass to error handler
}
});
router.post("/", async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await createUser(req.body);
res.status(StatusCodes.CREATED).json({ user });
} catch (err) {
next(err);
}
});
Winston Logger Setup#
Installation#
npm install winston
Logger Configuration#
import winston, { transports } from "winston";
import config from "../config.js";
const {
combine,
timestamp,
printf,
colorize,
align,
errors,
json,
} = winston.format;
// Custom colors
winston.addColors({
error: "red",
warn: "yellow",
info: "green",
http: "cyan",
debug: "magenta",
});
// File log format (JSON for production)
const fileLogFormat = combine(
timestamp(),
errors({ stack: true }),
json()
);
// Console log format (human-readable for development)
const consoleLogFormat = combine(
colorize({ all: true }),
timestamp({
format: "YYYY-MM-DD hh:mm:ss.SSS A",
}),
align(),
printf(({ timestamp, level, message, ...meta }) => {
const metaString = Object.keys(meta).length ? JSON.stringify(meta) : "";
return `${timestamp} ${level}: ${message} ${metaString}`;
})
);
const logger = winston.createLogger({
level: config.devMode ? "debug" : "http",
transports: [
new transports.File({
filename: "logs/error.log",
level: "error",
format: fileLogFormat,
}),
new transports.File({
filename: "logs/combined.log",
format: fileLogFormat,
}),
new transports.Console({
format: consoleLogFormat,
}),
],
exitOnError: false,
});
export default logger;
Logger Usage#
import logger from "./logger";
// Different log levels
logger.info("Server started on port 3000");
logger.error("Database connection failed");
logger.warn("API rate limit approaching");
logger.debug({ userId: 123, action: "login" });
logger.http("GET /api/users - 200");
// With metadata
logger.error("User creation failed", {
userId: "123",
email: "user@example.com",
error: err.message,
});
Morgan vs Winston#
Morgan = HTTP request logging - Logs every incoming request - Shows method, URL, status code, response time - Integrates with Winston for consistent logging
Winston = Application logging - Core debugging and monitoring - Error tracking - Custom log levels - File and console output
Integration#
import morgan from "morgan";
import logger from "./logger";
app.use(
morgan(config.devMode ? "dev" : "combined", {
stream: {
write: (msg: string) => {
logger.http(msg.trim());
},
},
})
);
Complete Flow Example#
// 1. Route with error handling
router.post("/users", async (req: Request, res: Response, next: NextFunction) => {
try {
const user = await createUser(req.body);
res.status(StatusCodes.CREATED).json({ user });
} catch (err) {
next(err); // Goes to error handler
}
});
// 2. Error is caught by global error handler
function errorHandler(err: Error, req: Request, res: Response, next: NextFunction) {
// Logs error with context
logger.error(err.message, {
endpoint: `${req.method} ${req.originalUrl}`,
requestor: { ip: getClientIp(req) },
err,
});
// Returns appropriate response
res.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: "Failed to create user",
code: "internal_server_error",
});
}
// 3. Morgan logs the request
// HTTP/1.1 POST /api/users - 500 - 42ms
Common Response Helpers#
export const commonResps = {
internalError: {
error: "Failed to complete request due to internal error",
code: "internal_server_error",
},
unauthorized: {
error: "Authentication required",
code: "unauthorized",
},
forbidden: {
error: "Insufficient permissions",
code: "forbidden",
},
notFound: {
error: "Resource not found",
code: "not_found",
},
conflict: {
error: "Resource already exists",
code: "resource_conflict",
},
};
// Usage
res.status(StatusCodes.UNAUTHORIZED).json(commonResps.unauthorized);
Database Error Handling#
import { DrizzleQueryError } from "drizzle-orm/errors";
try {
await createUser(data);
} catch (err) {
if (err instanceof DrizzleQueryError) {
// Unique constraint violation (e.g., duplicate email)
if ((err as any).cause?.code === "23505") {
return res.status(StatusCodes.CONFLICT).json({
error: "User with this email already exists",
code: "duplicate_email",
});
}
}
throw err;
}
Best Practices#
- ✅ Always use try-catch in async route handlers
- ✅ Pass errors to next() - Let global handler deal with it
- ✅ Log with context - Include request details, user info
- ✅ Hide stack traces in production - Only show in dev mode
- ✅ Use status codes constants - Import from
http-status-codes - ✅ Consistent error format - Use error code + message
- ✅ Database-specific errors - Handle unique constraints, foreign keys
Next Steps#
- Authentication - Handle auth errors
- Request Validation - Validate before processing