Error Handling
Comprehensive error handling utilities and patterns for Verb applications with proper error responses and debugging.
Error Classes
VerbError
Base error class for all Verb-specific errors:
typescript
import { VerbError } from "verb";
class VerbError extends Error {
statusCode: number;
code?: string;
details?: any;
constructor(
message: string,
statusCode: number = 500,
code?: string,
details?: any
) {
super(message);
this.name = "VerbError";
this.statusCode = statusCode;
this.code = code;
this.details = details;
}
}
// Usage
throw new VerbError("User not found", 404, "USER_NOT_FOUND");
ValidationError
Input validation errors:
typescript
import { ValidationError } from "verb";
class ValidationError extends VerbError {
constructor(message: string, field?: string, value?: any) {
super(message, 400, "VALIDATION_ERROR", { field, value });
this.name = "ValidationError";
}
}
// Usage
throw new ValidationError("Email is required", "email");
AuthenticationError
Authentication-related errors:
typescript
import { AuthenticationError } from "verb";
class AuthenticationError extends VerbError {
constructor(message: string = "Authentication required") {
super(message, 401, "AUTHENTICATION_ERROR");
this.name = "AuthenticationError";
}
}
// Usage
throw new AuthenticationError("Invalid token");
AuthorizationError
Authorization/permission errors:
typescript
import { AuthorizationError } from "verb";
class AuthorizationError extends VerbError {
constructor(message: string = "Insufficient permissions") {
super(message, 403, "AUTHORIZATION_ERROR");
this.name = "AuthorizationError";
}
}
// Usage
throw new AuthorizationError("Admin access required");
NotFoundError
Resource not found errors:
typescript
import { NotFoundError } from "verb";
class NotFoundError extends VerbError {
constructor(resource?: string) {
const message = resource ? `${resource} not found` : "Resource not found";
super(message, 404, "NOT_FOUND");
this.name = "NotFoundError";
}
}
// Usage
throw new NotFoundError("User");
ConflictError
Resource conflict errors:
typescript
import { ConflictError } from "verb";
class ConflictError extends VerbError {
constructor(message: string = "Resource conflict") {
super(message, 409, "CONFLICT");
this.name = "ConflictError";
}
}
// Usage
throw new ConflictError("Email already exists");
RateLimitError
Rate limiting errors:
typescript
import { RateLimitError } from "verb";
class RateLimitError extends VerbError {
retryAfter?: number;
constructor(message: string = "Rate limit exceeded", retryAfter?: number) {
super(message, 429, "RATE_LIMIT_EXCEEDED");
this.name = "RateLimitError";
this.retryAfter = retryAfter;
}
}
// Usage
throw new RateLimitError("Too many requests", 60);
Error Handler Middleware
Global Error Handler
Catch and handle all application errors:
typescript
import { ErrorHandler } from "verb";
const globalErrorHandler: ErrorHandler = (err, req, res, next) => {
// Log error for debugging
console.error("Error occurred:", {
message: err.message,
stack: err.stack,
url: req.url,
method: req.method,
ip: req.ip,
userAgent: req.get("user-agent")
});
// Handle different error types
if (err instanceof ValidationError) {
return res.status(400).json({
error: "Validation Error",
message: err.message,
code: err.code,
field: err.details?.field
});
}
if (err instanceof AuthenticationError) {
return res.status(401).json({
error: "Authentication Error",
message: err.message,
code: err.code
});
}
if (err instanceof AuthorizationError) {
return res.status(403).json({
error: "Authorization Error",
message: err.message,
code: err.code
});
}
if (err instanceof NotFoundError) {
return res.status(404).json({
error: "Not Found",
message: err.message,
code: err.code
});
}
if (err instanceof ConflictError) {
return res.status(409).json({
error: "Conflict",
message: err.message,
code: err.code
});
}
if (err instanceof RateLimitError) {
const response = {
error: "Rate Limit Exceeded",
message: err.message,
code: err.code
};
if (err.retryAfter) {
res.header("Retry-After", err.retryAfter.toString());
}
return res.status(429).json(response);
}
// Handle VerbError instances
if (err instanceof VerbError) {
return res.status(err.statusCode).json({
error: err.name,
message: err.message,
code: err.code,
details: err.details
});
}
// Handle syntax errors (JSON parsing, etc.)
if (err instanceof SyntaxError && "body" in err) {
return res.status(400).json({
error: "Bad Request",
message: "Invalid JSON in request body",
code: "INVALID_JSON"
});
}
// Handle multer errors (file upload)
if (err.code === "LIMIT_FILE_SIZE") {
return res.status(413).json({
error: "Payload Too Large",
message: "File size exceeds limit",
code: "FILE_TOO_LARGE"
});
}
// Default error response
const isDevelopment = process.env.NODE_ENV === "development";
res.status(500).json({
error: "Internal Server Error",
message: isDevelopment ? err.message : "Something went wrong",
code: "INTERNAL_ERROR",
...(isDevelopment && { stack: err.stack })
});
};
// Apply to app (must be last middleware)
app.use(globalErrorHandler);
404 Handler
Handle routes that don't exist:
typescript
const notFoundHandler = (req: VerbRequest, res: VerbResponse) => {
res.status(404).json({
error: "Not Found",
message: `Route ${req.method} ${req.path} not found`,
code: "ROUTE_NOT_FOUND"
});
};
// Apply after all routes
app.use("*", notFoundHandler);
Async Error Handling
Async Route Wrapper
Automatically catch async errors in route handlers:
typescript
const asyncHandler = (fn: RouteHandler): RouteHandler => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Usage
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await getUserById(req.params.id);
if (!user) {
throw new NotFoundError("User");
}
res.json(user);
}));
Try-Catch Alternative
Using the async handler eliminates repetitive try-catch blocks:
typescript
// Without async handler (repetitive)
app.get("/users/:id", async (req, res, next) => {
try {
const user = await getUserById(req.params.id);
if (!user) {
throw new NotFoundError("User");
}
res.json(user);
} catch (error) {
next(error);
}
});
// With async handler (clean)
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await getUserById(req.params.id);
if (!user) {
throw new NotFoundError("User");
}
res.json(user);
}));
Validation Error Handling
Field Validation
Handle multiple validation errors:
typescript
interface ValidationIssue {
field: string;
message: string;
value?: any;
}
class ValidationError extends VerbError {
issues: ValidationIssue[];
constructor(issues: ValidationIssue[]) {
const message = `Validation failed: ${issues.map(i => i.field).join(", ")}`;
super(message, 400, "VALIDATION_ERROR", { issues });
this.name = "ValidationError";
this.issues = issues;
}
}
// Usage
const validateUser = (data: any) => {
const issues: ValidationIssue[] = [];
if (!data.email) {
issues.push({ field: "email", message: "Email is required" });
} else if (!isValidEmail(data.email)) {
issues.push({
field: "email",
message: "Invalid email format",
value: data.email
});
}
if (!data.password) {
issues.push({ field: "password", message: "Password is required" });
} else if (data.password.length < 8) {
issues.push({
field: "password",
message: "Password must be at least 8 characters"
});
}
if (issues.length > 0) {
throw new ValidationError(issues);
}
};
app.post("/users", asyncHandler(async (req, res) => {
validateUser(req.body);
const user = await createUser(req.body);
res.status(201).json(user);
}));
Database Error Handling
Common Database Errors
Handle database-specific errors:
typescript
const handleDatabaseError = (error: any) => {
// MongoDB duplicate key error
if (error.code === 11000) {
const field = Object.keys(error.keyPattern)[0];
throw new ConflictError(`${field} already exists`);
}
// PostgreSQL unique violation
if (error.code === "23505") {
throw new ConflictError("Resource already exists");
}
// PostgreSQL foreign key violation
if (error.code === "23503") {
throw new ValidationError("Referenced resource does not exist");
}
// Connection errors
if (error.code === "ECONNREFUSED" || error.code === "ENOTFOUND") {
throw new VerbError("Database connection failed", 503, "DATABASE_UNAVAILABLE");
}
// Default database error
throw new VerbError("Database operation failed", 500, "DATABASE_ERROR");
};
// Usage in route
app.post("/users", asyncHandler(async (req, res) => {
try {
const user = await createUser(req.body);
res.status(201).json(user);
} catch (error) {
handleDatabaseError(error);
}
}));
API Error Responses
Standardized Error Format
Consistent error response structure:
typescript
interface ErrorResponse {
error: string; // Error type
message: string; // Human-readable message
code?: string; // Machine-readable code
details?: any; // Additional error details
timestamp: string; // ISO timestamp
path: string; // Request path
method: string; // HTTP method
requestId?: string; // Request tracking ID
}
const formatErrorResponse = (
err: Error,
req: VerbRequest
): ErrorResponse => {
return {
error: err.name || "Error",
message: err.message,
code: err instanceof VerbError ? err.code : undefined,
details: err instanceof VerbError ? err.details : undefined,
timestamp: new Date().toISOString(),
path: req.path,
method: req.method,
requestId: req.id
};
};
// Usage in error handler
const globalErrorHandler: ErrorHandler = (err, req, res, next) => {
const errorResponse = formatErrorResponse(err, req);
const statusCode = err instanceof VerbError ? err.statusCode : 500;
res.status(statusCode).json(errorResponse);
};
Logging & Monitoring
Error Logging
Log errors for monitoring and debugging:
typescript
import { logger } from "./logger";
const logError = (error: Error, req: VerbRequest) => {
const logData = {
error: {
name: error.name,
message: error.message,
stack: error.stack,
code: error instanceof VerbError ? error.code : undefined
},
request: {
method: req.method,
url: req.url,
headers: req.headers,
ip: req.ip,
userAgent: req.get("user-agent"),
body: req.body
},
timestamp: new Date().toISOString()
};
if (error instanceof VerbError && error.statusCode < 500) {
logger.warn("Client error", logData);
} else {
logger.error("Server error", logData);
}
};
// Usage in error handler
const globalErrorHandler: ErrorHandler = (err, req, res, next) => {
logError(err, req);
// ... handle error response
};
Error Metrics
Track error metrics for monitoring:
typescript
const errorMetrics = {
total: 0,
byType: new Map<string, number>(),
byStatusCode: new Map<number, number>(),
byRoute: new Map<string, number>()
};
const trackError = (error: Error, req: VerbRequest) => {
errorMetrics.total++;
// Track by error type
const type = error.name;
errorMetrics.byType.set(type, (errorMetrics.byType.get(type) || 0) + 1);
// Track by status code
const statusCode = error instanceof VerbError ? error.statusCode : 500;
errorMetrics.byStatusCode.set(statusCode, (errorMetrics.byStatusCode.get(statusCode) || 0) + 1);
// Track by route
const route = `${req.method} ${req.path}`;
errorMetrics.byRoute.set(route, (errorMetrics.byRoute.get(route) || 0) + 1);
};
// Expose metrics endpoint
app.get("/metrics/errors", (req, res) => {
res.json({
total: errorMetrics.total,
byType: Object.fromEntries(errorMetrics.byType),
byStatusCode: Object.fromEntries(errorMetrics.byStatusCode),
byRoute: Object.fromEntries(errorMetrics.byRoute)
});
});
Circuit Breaker Pattern
Prevent cascading failures:
typescript
class CircuitBreaker {
private failures = 0;
private lastFailTime = 0;
private state: "CLOSED" | "OPEN" | "HALF_OPEN" = "CLOSED";
constructor(
private threshold = 5,
private timeout = 60000
) {}
async execute<T>(operation: () => Promise<T>): Promise<T> {
if (this.state === "OPEN") {
if (Date.now() - this.lastFailTime > this.timeout) {
this.state = "HALF_OPEN";
} else {
throw new VerbError("Circuit breaker is OPEN", 503, "SERVICE_UNAVAILABLE");
}
}
try {
const result = await operation();
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
private onSuccess() {
this.failures = 0;
this.state = "CLOSED";
}
private onFailure() {
this.failures++;
this.lastFailTime = Date.now();
if (this.failures >= this.threshold) {
this.state = "OPEN";
}
}
}
// Usage
const dbCircuitBreaker = new CircuitBreaker(5, 30000);
app.get("/users", asyncHandler(async (req, res) => {
const users = await dbCircuitBreaker.execute(() => getUsersFromDB());
res.json(users);
}));
Testing Error Handling
Error Handler Tests
typescript
import { test, expect } from "bun:test";
import request from "supertest";
test("handles validation errors", async () => {
const response = await request(app)
.post("/users")
.send({}) // Empty body
.expect(400);
expect(response.body.error).toBe("Validation Error");
expect(response.body.code).toBe("VALIDATION_ERROR");
});
test("handles not found errors", async () => {
const response = await request(app)
.get("/users/nonexistent")
.expect(404);
expect(response.body.error).toBe("Not Found");
expect(response.body.message).toContain("User not found");
});
test("handles authentication errors", async () => {
const response = await request(app)
.get("/protected")
.expect(401);
expect(response.body.error).toBe("Authentication Error");
});
Best Practices
- Use Specific Error Types: Create custom error classes for different scenarios
- Consistent Error Format: Use standardized error response structure
- Proper Status Codes: Use appropriate HTTP status codes
- Log Errors: Log errors for debugging and monitoring
- Don't Expose Sensitive Data: Never expose internal details in production
- Handle Async Errors: Use async wrappers to catch Promise rejections
- Graceful Degradation: Implement fallback mechanisms for critical services
See Also
- Middleware API - Creating error handling middleware
- Validation - Input validation utilities
- Security - Security best practices
- Testing - Testing error scenarios