Error Handling Examples
This document provides practical examples of the improved error handling system.
Table of Contents
Before and After Comparison
Example 1: Get By ID
Before:
async getById(req: Request, res: Response) {
try {
const id = Number(req.params.id);
if (isNaN(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
const item = await Service.getById(id);
if (!item) {
return res.status(404).json({ error: 'Not found' });
}
res.json(item);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Internal error' });
}
}
After:
async getById(req: Request, res: Response, next: NextFunction) {
try {
const id = validateId(req.params.id, 'id');
const item = await Service.getById(id);
if (!item) {
throw new NotFoundError('Item', id);
}
res.json({ success: true, data: item });
} catch (err) {
next(err);
}
}
Benefits:
- ✅ Cleaner code with less boilerplate
- ✅ Consistent error responses
- ✅ Better error messages
- ✅ Type-safe validation
- ✅ Centralized error handling
Example 2: Create Resource
Before:
async create(req: Request, res: Response) {
try {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Missing required fields' });
}
const item = await Service.create({ name, email });
res.status(201).json(item);
} catch (err) {
console.error(err);
if (err.message.includes('duplicate')) {
res.status(409).json({ error: 'Already exists' });
} else {
res.status(500).json({ error: 'Failed to create' });
}
}
}
After:
async create(req: Request, res: Response, next: NextFunction) {
try {
validateRequiredFields(req.body, ['name', 'email']);
const name = validateAndSanitizeString(req.body.name, 'name', {
required: true,
max: 255,
});
const email = validateAndSanitizeString(req.body.email, 'email', {
required: true,
max: 255,
});
validateEmail(email!, 'email');
const item = await Service.create({ name: name!, email: email! });
res.status(201).json({ success: true, data: item });
} catch (err) {
if (err instanceof ValidationError) {
next(err);
} else {
next(new DatabaseError('Failed to create item', err as Error));
}
}
}
Benefits:
- ✅ Comprehensive validation
- ✅ Input sanitization
- ✅ Detailed error messages
- ✅ Proper error classification
- ✅ Security improvements
Common Use Cases
1. Validating Route Parameters
// Using validation utility
async getById(req: Request, res: Response, next: NextFunction) {
try {
const id = validateId(req.params.id, 'id');
// ... rest of logic
} catch (err) {
next(err);
}
}
// Using middleware
router.get('/:id', validateIdParam('id'), controller.getById);
2. Validating Required Fields
async create(req: Request, res: Response, next: NextFunction) {
try {
// Validate all required fields at once
validateRequiredFields(req.body, ['name', 'email', 'password']);
// ... rest of logic
} catch (err) {
next(err);
}
}
// Or using middleware
router.post('/',
validateRequiredBodyFields(['name', 'email', 'password']),
controller.create
);
3. Validating and Sanitizing Strings
async create(req: Request, res: Response, next: NextFunction) {
try {
const name = validateAndSanitizeString(req.body.name, 'name', {
required: true,
min: 2,
max: 100,
});
const description = validateAndSanitizeString(req.body.description, 'description', {
required: false,
max: 500,
});
// ... rest of logic
} catch (err) {
next(err);
}
}
4. Validating JSON Configuration
async create(req: Request, res: Response, next: NextFunction) {
try {
const { config } = req.body;
if (config) {
const parsed = validateJSON(config, 'config');
// Use parsed object
}
// ... rest of logic
} catch (err) {
next(err);
}
}
5. Handling Not Found
async getById(req: Request, res: Response, next: NextFunction) {
try {
const id = validateId(req.params.id, 'id');
const item = await Service.getById(id);
if (!item) {
throw new NotFoundError('Item', id);
}
res.json({ success: true, data: item });
} catch (err) {
next(err);
}
}
6. Handling Database Errors
async create(req: Request, res: Response, next: NextFunction) {
try {
// ... validation
const item = await Service.create(data);
res.status(201).json({ success: true, data: item });
} catch (err) {
if (err instanceof ValidationError) {
next(err);
} else {
next(new DatabaseError('Failed to create item', err as Error));
}
}
}
7. Handling External Service Errors
async getPullRequestDetails(req: Request, res: Response, next: NextFunction) {
try {
const id = validateId(req.params.id, 'id');
const pr = await PRService.getById(id);
if (!pr) {
throw new NotFoundError('Pull Request', id);
}
const details = await GitHubService.getPRDetails(pr.repo, pr.number);
res.json({ success: true, data: details });
} catch (err) {
if (err instanceof ValidationError || err instanceof NotFoundError) {
next(err);
} else {
next(new ExternalServiceError('GitHub', 'Failed to fetch PR details', err as Error));
}
}
}
Error Response Examples
1. Validation Error
Request:
POST /api/todos
Content-Type: application/json
{
"description": "Some description"
}
Response:
{
"success": false,
"error": {
"message": "Missing or invalid required fields",
"code": "VALIDATION_ERROR",
"statusCode": 400,
"details": [
{
"field": "title",
"message": "title is required",
"code": "REQUIRED_FIELD"
}
],
"timestamp": "2024-01-20T10:30:00.000Z",
"path": "/api/todos",
"method": "POST"
}
}
2. Not Found Error
Request:
GET /api/todos/999
Response:
{
"success": false,
"error": {
"message": "Todo with id '999' not found",
"code": "NOT_FOUND",
"statusCode": 404,
"timestamp": "2024-01-20T10:30:00.000Z",
"path": "/api/todos/999",
"method": "GET"
}
}
3. Invalid ID Error
Request:
GET /api/todos/invalid
Response:
{
"success": false,
"error": {
"message": "Invalid id",
"code": "VALIDATION_ERROR",
"statusCode": 400,
"details": [
{
"field": "id",
"message": "id must be a positive integer",
"code": "INVALID_ID",
"value": "invalid"
}
],
"timestamp": "2024-01-20T10:30:00.000Z",
"path": "/api/todos/invalid",
"method": "GET"
}
}
4. Conflict Error
Request:
POST /api/apps
Content-Type: application/json
{
"name": "My App",
"code": "existing-code"
}
Response:
{
"success": false,
"error": {
"message": "App code must be unique",
"code": "CONFLICT",
"statusCode": 409,
"timestamp": "2024-01-20T10:30:00.000Z",
"path": "/api/apps",
"method": "POST"
}
}
5. Invalid JSON Error
Request:
POST /api/apps
Content-Type: application/json
{
"name": "My App",
"code": "my-app",
"e2eTriggerConfiguration": "{ invalid json }"
}
Response:
{
"success": false,
"error": {
"message": "Invalid JSON in e2eTriggerConfiguration",
"code": "VALIDATION_ERROR",
"statusCode": 400,
"details": [
{
"field": "e2eTriggerConfiguration",
"message": "e2eTriggerConfiguration must be valid JSON",
"code": "INVALID_JSON",
"value": "{ invalid json }"
}
],
"timestamp": "2024-01-20T10:30:00.000Z",
"path": "/api/apps",
"method": "POST"
}
}
6. Success Response
Request:
GET /api/todos/1
Response:
{
"success": true,
"data": {
"id": 1,
"title": "Complete documentation",
"description": "Write comprehensive docs",
"link": "https://github.com/...",
"dueDate": "2024-01-25T00:00:00.000Z",
"isCompleted": false
}
}
Validation Examples
String Validation
// Required string with max length
const name = validateAndSanitizeString(req.body.name, 'name', {
required: true,
max: 255,
});
// Optional string with min and max length
const description = validateAndSanitizeString(req.body.description, 'description', {
required: false,
min: 10,
max: 1000,
});
Email Validation
const email = validateAndSanitizeString(req.body.email, 'email', {
required: true,
max: 255,
});
validateEmail(email!, 'email');
URL Validation
const url = validateAndSanitizeString(req.body.url, 'url', {
required: false,
max: 500,
});
if (url) {
validateURL(url, 'url');
}
Date Validation
const dueDate = req.body.dueDate;
if (dueDate) {
const parsedDate = validateDate(dueDate, 'dueDate');
// Use parsedDate
}
Enum Validation
const status = validateEnum(
req.body.status,
['pending', 'active', 'completed'],
'status'
);
Array Validation
const tags = validateArray(req.body.tags, 'tags', {
minLength: 1,
maxLength: 10,
});
Tips and Best Practices
- Always validate input early in the controller before any business logic
- Use specific error classes for different scenarios
- Provide detailed error information to help API consumers
- Sanitize user input to prevent injection attacks
- Use consistent response format across all endpoints
- Log errors with context for debugging
- Don't expose sensitive information in error messages
- Use middleware for common validations to reduce boilerplate
- Test error scenarios to ensure proper error handling
- Document error responses in API documentation