Error Handling and Logging
Error handling and logging are critical for maintaining production applications. This article covers best practices.
Introduction
Error handling and logging provide:
- Error tracking
- Debugging capabilities
- Performance monitoring
- Audit trails
- System reliability
Understanding error handling helps you:
- Catch and handle errors
- Debug issues
- Monitor applications
- Improve reliability
- Maintain code quality
Error Handling Strategies
Try-Catch Blocks
// โ
Good: Basic try-catch
try {
const data = JSON.parse(jsonString);
console.log(data);
} catch (err) {
console.error('Error parsing JSON:', err.message);
}
// โ
Good: Try-catch with finally
try {
const file = fs.readFileSync('file.txt', 'utf-8');
console.log(file);
} catch (err) {
console.error('Error reading file:', err);
} finally {
console.log('Operation completed');
}
// โ
Good: Async try-catch
async function fetchData() {
try {
const response = await fetch('/api/data');
const data = await response.json();
return data;
} catch (err) {
console.error('Error fetching data:', err);
throw err;
}
}
// โ
Good: Multiple catch blocks
try {
// Some operation
} catch (err) {
if (err instanceof TypeError) {
console.error('Type error:', err);
} else if (err instanceof ReferenceError) {
console.error('Reference error:', err);
} else {
console.error('Unknown error:', err);
}
}
Custom Error Classes
// โ
Good: Custom error class
class AppError extends Error {
constructor(message, status = 500) {
super(message);
this.status = status;
this.timestamp = new Date();
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
this.name = 'NotFoundError';
}
}
class UnauthorizedError extends AppError {
constructor(message) {
super(message, 401);
this.name = 'UnauthorizedError';
}
}
// โ
Good: Throw custom errors
function validateUser(user) {
if (!user.email) {
throw new ValidationError('Email is required');
}
if (!user.password) {
throw new ValidationError('Password is required');
}
}
async function getUser(id) {
const user = await User.findById(id);
if (!user) {
throw new NotFoundError('User not found');
}
return user;
}
module.exports = { AppError, ValidationError, NotFoundError, UnauthorizedError };
Error Handling Middleware
// middleware/errorHandler.js
const { AppError } = require('./errors');
const errorHandler = (err, req, res, next) => {
console.error('Error:', err);
// Default error
let status = err.status || 500;
let message = err.message || 'Internal server error';
// Handle specific errors
if (err.name === 'ValidationError') {
status = 400;
} else if (err.name === 'CastError') {
status = 400;
message = 'Invalid ID format';
} else if (err.name === 'MongoError' && err.code === 11000) {
status = 400;
message = 'Duplicate field value';
}
res.status(status).json({
error: {
status,
message,
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
}
});
};
const asyncHandler = (fn) => {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
module.exports = { errorHandler, asyncHandler };
// routes/users.js
const { asyncHandler } = require('../middleware/errorHandler');
const { NotFoundError } = require('../errors');
router.get('/:id', asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new NotFoundError('User not found');
}
res.json(user);
}));
// app.js
const { errorHandler } = require('./middleware/errorHandler');
app.use('/api', routes);
app.use(errorHandler);
Logging
Console Logging
// โ
Good: Different log levels
console.log('Info message'); // General information
console.warn('Warning message'); // Warning
console.error('Error message'); // Error
// โ
Good: Structured logging
console.log(JSON.stringify({
timestamp: new Date(),
level: 'info',
message: 'User logged in',
userId: 123
}));
// โ
Good: Log with context
function logRequest(req, res, next) {
console.log({
timestamp: new Date(),
method: req.method,
url: req.url,
ip: req.ip
});
next();
}
Winston Logger
// โ
Good: Install Winston
// npm install winston
const winston = require('winston');
// โ
Good: Create logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'error.log', level: 'error' }),
new winston.transports.File({ filename: 'combined.log' })
]
});
// โ
Good: Add console transport in development
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// โ
Good: Use logger
logger.info('Application started');
logger.warn('Warning message');
logger.error('Error occurred', { error: err });
// โ
Good: Logger middleware
function loggerMiddleware(req, res, next) {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info({
method: req.method,
url: req.url,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip
});
});
next();
}
module.exports = logger;
Morgan Logger
// โ
Good: Install Morgan
// npm install morgan
const morgan = require('morgan');
const fs = require('fs');
const path = require('path');
const app = express();
// โ
Good: Use Morgan middleware
app.use(morgan('combined'));
// โ
Good: Log to file
const accessLogStream = fs.createWriteStream(
path.join(__dirname, 'access.log'),
{ flags: 'a' }
);
app.use(morgan('combined', { stream: accessLogStream }));
// โ
Good: Custom Morgan format
morgan.token('user-id', (req) => req.user?.id || 'anonymous');
app.use(morgan(':user-id :method :url :status :response-time ms'));
Debugging
Debug Module
// โ
Good: Install debug
// npm install debug
const debug = require('debug')('app:*');
// โ
Good: Use debug
debug('Application started');
debug('User logged in:', userId);
debug('Database query:', query);
// โ
Good: Namespace debugging
const debugAuth = require('debug')('app:auth');
const debugDB = require('debug')('app:db');
debugAuth('User authentication');
debugDB('Database connection');
// Run with: DEBUG=app:* node app.js
// Or: DEBUG=app:auth node app.js
Error Tracking Services
// โ
Good: Install Sentry
// npm install @sentry/node
const Sentry = require('@sentry/node');
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0
});
const app = express();
app.use(Sentry.Handlers.requestHandler());
// Routes
app.get('/', (req, res) => {
res.json({ message: 'Hello' });
});
app.use(Sentry.Handlers.errorHandler());
// โ
Good: Capture exceptions
try {
// Some operation
} catch (err) {
Sentry.captureException(err);
}
// โ
Good: Capture messages
Sentry.captureMessage('Something went wrong', 'warning');
Monitoring and Alerting
Health Checks
// โ
Good: Health check endpoint
app.get('/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date(),
uptime: process.uptime()
});
});
// โ
Good: Detailed health check
app.get('/health/detailed', async (req, res) => {
try {
const dbHealth = await checkDatabaseHealth();
const cacheHealth = await checkCacheHealth();
res.json({
status: 'ok',
database: dbHealth,
cache: cacheHealth,
timestamp: new Date()
});
} catch (err) {
res.status(503).json({
status: 'error',
error: err.message
});
}
});
// โ
Good: Readiness check
app.get('/ready', async (req, res) => {
try {
await checkDatabaseConnection();
res.json({ ready: true });
} catch (err) {
res.status(503).json({ ready: false });
}
});
Best Practices
-
Log at appropriate levels:
// โ Good: Appropriate log levels logger.info('User logged in'); logger.warn('Deprecated API used'); logger.error('Database connection failed'); // โ Bad: Wrong log levels logger.error('User logged in'); logger.info('Database connection failed'); -
Include context in logs:
// โ Good: Include context logger.error('Error processing request', { userId: req.user?.id, requestId: req.id, error: err.message }); // โ Bad: No context logger.error('Error occurred'); -
Handle errors gracefully:
// โ Good: Graceful error handling try { await operation(); } catch (err) { logger.error('Operation failed', err); res.status(500).json({ error: 'Operation failed' }); } // โ Bad: Crash on error await operation();
Summary
Error handling and logging are essential. Key takeaways:
- Use try-catch for error handling
- Create custom error classes
- Implement error handling middleware
- Use logging frameworks
- Monitor applications
- Include context in logs
- Handle errors gracefully
- Track errors in production
Related Resources
Next Steps
- Learn about Deployment
- Explore Full-Stack Development
- Study API Design
- Practice error handling
- Build reliable applications
Comments