Custom Error Classes
```typescript
// errors/app-error.ts
export class AppError extends Error {
constructor(
public code: string,
public message: string,
public statusCode: number = 500,
public details?: Record,
public isOperational: boolean = true
) {
super(message);
this.name = 'AppError';
Error.captureStackTrace(this, this.constructor);
}
}
// Common error types
export class NotFoundError extends AppError {
constructor(resource: string, id?: string) {
super(
'RESOURCE_NOT_FOUND',
${resource} not found,
404,
id ? { [${resource.toLowerCase()}Id]: id } : undefined
);
}
}
export class ValidationError extends AppError {
constructor(details: Array<{ field: string; message: string }>) {
super('VALIDATION_ERROR', 'Validation failed', 400, { errors: details });
}
}
export class UnauthorizedError extends AppError {
constructor(message = 'Authentication required') {
super('UNAUTHORIZED', message, 401);
}
}
export class ForbiddenError extends AppError {
constructor(message = 'Access denied') {
super('FORBIDDEN', message, 403);
}
}
export class ConflictError extends AppError {
constructor(message: string, details?: Record) {
super('CONFLICT', message, 409, details);
}
}
export class RateLimitError extends AppError {
constructor(retryAfter: number) {
super('RATE_LIMITED', 'Too many requests', 429, { retryAfter });
}
}
export class ExternalServiceError extends AppError {
constructor(service: string, originalError?: Error) {
super(
'EXTERNAL_SERVICE_ERROR',
${service} service unavailable,
503,
{ service, originalMessage: originalError?.message }
);
}
}
```
Error Handler Middleware
```typescript
// middleware/error-handler.ts
import { Request, Response, NextFunction } from 'express';
import { AppError } from '../errors/app-error';
import { logger } from '../utils/logger';
interface ErrorResponse {
error: {
code: string;
message: string;
details?: Record;
requestId?: string;
};
}
export function errorHandler(
err: Error,
req: Request,
res: Response,
next: NextFunction
) {
const requestId = req.headers['x-request-id'] as string;
// Handle known operational errors
if (err instanceof AppError) {
logger.warn('Operational error', {
code: err.code,
message: err.message,
statusCode: err.statusCode,
requestId,
path: req.path,
});
const response: ErrorResponse = {
error: {
code: err.code,
message: err.message,
details: err.details,
requestId,
},
};
return res.status(err.statusCode).json(response);
}
// Handle Prisma errors
if (err.name === 'PrismaClientKnownRequestError') {
const prismaError = err as any;
if (prismaError.code === 'P2002') {
return res.status(409).json({
error: {
code: 'DUPLICATE_ENTRY',
message: 'Resource already exists',
details: { fields: prismaError.meta?.target },
requestId,
},
});
}
if (prismaError.code === 'P2025') {
return res.status(404).json({
error: {
code: 'RESOURCE_NOT_FOUND',
message: 'Resource not found',
requestId,
},
});
}
}
// Handle unknown errors (programming errors)
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
requestId,
path: req.path,
});
// Don't leak error details in production
const message = process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message;
return res.status(500).json({
error: {
code: 'INTERNAL_ERROR',
message,
requestId,
},
});
}
```
Async Handler Wrapper
```typescript
// utils/async-handler.ts
import { Request, Response, NextFunction, RequestHandler } from 'express';
type AsyncRequestHandler = (
req: Request,
res: Response,
next: NextFunction
) => Promise;
export function asyncHandler(fn: AsyncRequestHandler): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
}
// Usage
router.get('/users/:id', asyncHandler(async (req, res) => {
const user = await userService.findById(req.params.id);
if (!user) {
throw new NotFoundError('User', req.params.id);
}
res.json(user);
}));
```
Service Layer Error Handling
```typescript
// services/user-service.ts
import { NotFoundError, ConflictError } from '../errors/app-error';
class UserService {
async findById(id: string): Promise {
const user = await db.users.findUnique({ where: { id } });
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
async create(data: CreateUserInput): Promise {
const existing = await db.users.findUnique({ where: { email: data.email } });
if (existing) {
throw new ConflictError('Email already registered', { email: data.email });
}
return db.users.create({ data });
}
async updateEmail(userId: string, newEmail: string): Promise {
try {
return await db.users.update({
where: { id: userId },
data: { email: newEmail },
});
} catch (error) {
if (error.code === 'P2002') {
throw new ConflictError('Email already in use');
}
throw error;
}
}
}
```