Error Handling & Debugging
Robust error handling is what separates amateur applications from production-ready systems. In this chapter, you’ll learn how to handle errors gracefully, debug effectively, and implement logging strategies.Understanding Error Types
Operational vs Programming Errors
| Type | Description | Examples | Handling |
|---|---|---|---|
| Operational | Runtime problems you can anticipate | Network failures, invalid user input, database down | Handle gracefully, retry, inform user |
| Programming | Bugs in your code | TypeError, undefined access, wrong API usage | Fix the code, crash and restart |
Critical Distinction: Operational errors should be handled. Programming errors should crash the app (in production, use a process manager to restart).
The Error Class
Copy
// Built-in Error types
const err1 = new Error('Something went wrong');
const err2 = new TypeError('Expected a string');
const err3 = new RangeError('Value out of range');
const err4 = new SyntaxError('Invalid syntax');
// Error properties
console.log(err1.message); // 'Something went wrong'
console.log(err1.name); // 'Error'
console.log(err1.stack); // Stack trace
Custom Error Classes
Copy
// Base application error
class AppError extends Error {
constructor(message, statusCode, isOperational = true) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = isOperational;
Error.captureStackTrace(this, this.constructor);
}
}
// Specific error types
class NotFoundError extends AppError {
constructor(resource = 'Resource') {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message, errors = []) {
super(message, 400);
this.errors = errors;
}
}
class UnauthorizedError extends AppError {
constructor(message = 'Unauthorized') {
super(message, 401);
}
}
class ForbiddenError extends AppError {
constructor(message = 'Forbidden') {
super(message, 403);
}
}
class ConflictError extends AppError {
constructor(message = 'Conflict') {
super(message, 409);
}
}
// Usage
throw new NotFoundError('User');
throw new ValidationError('Validation failed', [
{ field: 'email', message: 'Invalid email format' }
]);
Async Error Handling Patterns
Callbacks (Legacy)
Copy
// Error-first callback pattern
fs.readFile('file.txt', (err, data) => {
if (err) {
console.error('Error reading file:', err);
return;
}
console.log(data);
});
Promises
Copy
// With .catch()
fetchData()
.then(data => process(data))
.then(result => save(result))
.catch(err => {
console.error('Error:', err);
});
// Promise.allSettled for multiple operations
const results = await Promise.allSettled([
fetchUser(1),
fetchUser(2),
fetchUser(3)
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`User ${index + 1}:`, result.value);
} else {
console.error(`User ${index + 1} failed:`, result.reason);
}
});
Async/Await
Copy
// Basic try/catch
async function getData() {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new AppError('Failed to fetch data', response.status);
}
return await response.json();
} catch (error) {
if (error.isOperational) {
// Handle operational error
console.error('Operational error:', error.message);
} else {
// Re-throw programming errors
throw error;
}
}
}
// Wrapper function for cleaner code
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
// Usage in Express
app.get('/users/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
}));
Express Error Handling
Centralized Error Handler
Copy
// middleware/errorHandler.js
const errorHandler = (err, req, res, next) => {
let error = { ...err };
error.message = err.message;
error.stack = err.stack;
// Log error
console.error('Error:', {
message: err.message,
stack: err.stack,
url: req.originalUrl,
method: req.method,
ip: req.ip,
userId: req.user?.id
});
// Mongoose bad ObjectId
if (err.name === 'CastError') {
error = new AppError('Invalid ID format', 400);
}
// Mongoose duplicate key
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
error = new ConflictError(`${field} already exists`);
}
// Mongoose validation error
if (err.name === 'ValidationError') {
const errors = Object.values(err.errors).map(e => ({
field: e.path,
message: e.message
}));
error = new ValidationError('Validation failed', errors);
}
// JWT errors
if (err.name === 'JsonWebTokenError') {
error = new UnauthorizedError('Invalid token');
}
if (err.name === 'TokenExpiredError') {
error = new UnauthorizedError('Token expired');
}
// Development vs Production responses
if (process.env.NODE_ENV === 'development') {
res.status(error.statusCode || 500).json({
success: false,
error: error.message,
stack: error.stack,
errors: error.errors
});
} else {
// Production: Don't leak error details
if (error.isOperational) {
res.status(error.statusCode).json({
success: false,
error: error.message,
...(error.errors && { errors: error.errors })
});
} else {
// Programming error: send generic message
console.error('UNEXPECTED ERROR:', err);
res.status(500).json({
success: false,
error: 'Something went wrong'
});
}
}
};
module.exports = errorHandler;
404 Handler
Copy
// Handle undefined routes
app.use((req, res, next) => {
next(new NotFoundError(`Route ${req.originalUrl} not found`));
});
// Error handler (must be last)
app.use(errorHandler);
Global Error Handlers
Copy
// Uncaught exceptions (synchronous code)
process.on('uncaughtException', (err) => {
console.error('UNCAUGHT EXCEPTION! 💥 Shutting down...');
console.error(err.name, err.message);
console.error(err.stack);
process.exit(1);
});
// Unhandled promise rejections
process.on('unhandledRejection', (reason, promise) => {
console.error('UNHANDLED REJECTION! 💥 Shutting down...');
console.error(reason);
// Close server gracefully, then exit
server.close(() => {
process.exit(1);
});
});
// SIGTERM handling (Heroku, Docker, etc.)
process.on('SIGTERM', () => {
console.log('👋 SIGTERM received. Shutting down gracefully');
server.close(() => {
console.log('💥 Process terminated');
});
});
Debugging Techniques
Console Methods
Copy
// Basic logging
console.log('Info message');
console.error('Error message');
console.warn('Warning message');
// Formatted output
console.table([
{ name: 'John', age: 25 },
{ name: 'Jane', age: 30 }
]);
// Timing
console.time('operation');
// ... some operation
console.timeEnd('operation'); // operation: 123ms
// Stack trace
console.trace('Trace point');
// Assertions
console.assert(1 === 2, 'Values are not equal');
// Grouping
console.group('User Data');
console.log('Name: John');
console.log('Age: 25');
console.groupEnd();
Node.js Debugger
Copy
# Built-in debugger
node inspect app.js
# Chrome DevTools
node --inspect app.js
# Then open chrome://inspect in Chrome
# Break on first line
node --inspect-brk app.js
VS Code Debugging
Copy
// .vscode/launch.json
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug App",
"program": "${workspaceFolder}/src/server.js",
"env": {
"NODE_ENV": "development"
},
"restart": true,
"console": "integratedTerminal"
},
{
"type": "node",
"request": "attach",
"name": "Attach to Process",
"port": 9229
}
]
}
Logging with Winston
Copy
npm install winston
Copy
// config/logger.js
const winston = require('winston');
const path = require('path');
const levels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4
};
const colors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'blue'
};
winston.addColors(colors);
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
if (Object.keys(meta).length) {
log += ` ${JSON.stringify(meta)}`;
}
if (stack) {
log += `\n${stack}`;
}
return log;
})
);
const transports = [
// Console output
new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize({ all: true }),
format
)
}),
// Error log file
new winston.transports.File({
filename: path.join('logs', 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
// Combined log file
new winston.transports.File({
filename: path.join('logs', 'combined.log'),
maxsize: 5242880,
maxFiles: 5
})
];
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
levels,
format,
transports,
exitOnError: false
});
module.exports = logger;
HTTP Request Logging with Morgan
Copy
const morgan = require('morgan');
const logger = require('./config/logger');
// Stream for Morgan to use Winston
const stream = {
write: (message) => logger.http(message.trim())
};
// Custom token
morgan.token('body', (req) => JSON.stringify(req.body));
// Use in Express
app.use(morgan(
':method :url :status :res[content-length] - :response-time ms',
{ stream }
));
Structured Logging
Copy
// Log with context
logger.info('User logged in', {
userId: user.id,
email: user.email,
ip: req.ip,
userAgent: req.headers['user-agent']
});
logger.error('Database connection failed', {
host: dbConfig.host,
error: err.message
});
// Request context middleware
const requestLogger = (req, res, next) => {
req.logger = logger.child({
requestId: req.headers['x-request-id'] || uuid(),
path: req.path,
method: req.method
});
next();
};
// Usage
app.get('/users', (req, res) => {
req.logger.info('Fetching users');
// ...
});
Summary
- Distinguish between operational and programming errors
- Create custom error classes for different error types
- Use async wrappers to catch errors in async routes
- Implement a centralized error handler in Express
- Handle uncaught exceptions and unhandled rejections
- Use Winston for structured, configurable logging
- Debug with VS Code or Chrome DevTools
- Log meaningful context with each error