ExpressJS Error Handling Debugging & Best Practices
Imagine you're driving a car. When something goes wrong, a warning light appears on the dashboard telling you what's wrong. In Express applications, error handling is like that dashboard – it catches problems and tells you (and your users) what went wrong. Good error handling separates professional applications from amateur ones.
Types of Errors in Express
- Operational Errors – Expected errors like invalid input, database connection failure, file not found.
- Programming Errors – Bugs in code like undefined variables, syntax errors, incorrect logic.
Basic Error Handling with try/catch
app.get('/user/:id', async (req, res) => {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
console.error('Error fetching user:', error);
res.status(500).json({ error: 'Internal server error' });
}
});Centralized Error Handling Middleware
Instead of try/catch in every route, you can create a centralized error handler.
// Custom error class
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
// Async wrapper to catch errors
const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next);
};
};
// Route using async wrapper
app.get('/user/:id', catchAsync(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) {
throw new AppError('User not found', 404);
}
res.json(user);
}));
// 404 handler for unmatched routes
app.use((req, res, next) => {
next(new AppError(`Can't find ${req.originalUrl} on this server`, 404));
});
// Global error handling middleware
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
// Development vs Production error response
if (process.env.NODE_ENV === 'development') {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production: don't leak error details
res.status(err.statusCode).json({
status: err.status,
message: err.isOperational ? err.message : 'Something went wrong'
});
}
});Handling Specific Error Types
// MongoDB duplicate key error
const handleDuplicateKeyError = (err) => {
const field = Object.keys(err.keyPattern)[0];
const message = `${field} already exists. Please use another value.`;
return new AppError(message, 400);
};
// MongoDB validation error
const handleValidationError = (err) => {
const errors = Object.values(err.errors).map(el => el.message);
const message = `Invalid input data: ${errors.join('. ')}`;
return new AppError(message, 400);
};
// JWT errors
const handleJWTError = () =>
new AppError('Invalid token. Please log in again.', 401);
const handleJWTExpired = () =>
new AppError('Your token has expired. Please log in again.', 401);Debugging Techniques
1. Using console.log (Basic)
console.log('Value:', variable);
console.log('Request body:', req.body);
console.log('Error:', error);2. Using Debug Module (Professional)
npm install debugconst debug = require('debug')('app:server');
const debugDb = require('debug')('app:database');
debug('Server starting on port 3000');
debugDb('Connected to MongoDB');Run with:
DEBUG=app:* node app.js3. Using Node.js Inspector
node --inspect app.jsThen open Chrome and go to
chrome://inspectBest Practices for Express Applications
1. Project Structure
src/
├── controllers/ # Route handlers
├── models/ # Database models
├── routes/ # Route definitions
├── middleware/ # Custom middleware
├── utils/ # Helper functions
├── config/ # Configuration files
├── services/ # Business logic
├── validators/ # Input validation
└── app.js # App entry point2. Environment Variables
// config/config.js
require('dotenv').config();
module.exports = {
port: process.env.PORT || 3000,
dbUrl: process.env.DATABASE_URL,
jwtSecret: process.env.JWT_SECRET,
nodeEnv: process.env.NODE_ENV
};3. Input Validation
const { body, validationResult } = require('express-validator');
app.post('/register',
body('email').isEmail().normalizeEmail(),
body('password').isLength({ min: 6 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
// Proceed
}
);4. Logging with Morgan
npm install morganconst morgan = require('morgan');
if (process.env.NODE_ENV === 'development') {
app.use(morgan('dev'));
}5. Use HTTP Status Codes Correctly
- 200 – OK (success)
- 201 – Created (POST success)
- 400 – Bad Request (client error)
- 401 – Unauthorized (not authenticated)
- 403 – Forbidden (authenticated but not allowed)
- 404 – Not Found
- 500 – Internal Server Error
Two Minute Drill
- Use centralized error handling with a global error middleware.
- Create custom error classes for different error types.
- Use catchAsync wrapper to avoid try/catch in every route.
- Differentiate between development and production error responses.
- Use debug module or morgan for logging.
- Organize code with proper project structure.
- Always validate input and use environment variables.
Need more clarification?
Drop us an email at career@quipoinfotech.com
