Error Handling and Validation
Why Error Handling Is a First-Class Concern
Every API will encounter errors: invalid inputs, missing records, database failures, network timeouts. How your API responds to those errors determines how reliable and developer-friendly it is. Poor error handling means cryptic messages, leaking stack traces to clients, and hours of debugging.
Professional backends treat error handling as architecture — not an afterthought.
Operational Errors vs Programmer Errors
Before writing error-handling code, understand the distinction:
- Operational errors are expected runtime failures: a user sends an invalid email, a record is not found, a third-party API is unavailable. These should be caught and returned as clean HTTP responses.
- Programmer errors are bugs in your code: accessing a property on
undefined, a typo in a variable name, an unhandled promise rejection. These should crash the process (in development) so you find and fix them, or be caught by a global handler (in production) and logged.
Never swallow programmer errors silently — they hide bugs.
A Custom Error Class
Node.js has a built-in Error class, but it does not carry an HTTP status code. Creating a custom class solves that:
class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // helps distinguish from programmer errors
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
Now you can throw meaningful errors anywhere in your codebase:
const AppError = require('./utils/AppError');
const user = await User.findById(id);
if (!user) {
throw new AppError('User not found', 404);
}
The Centralised Error Handler
Express has a special four-argument middleware for errors. All errors funnel into this single place:
// errorHandler.js
module.exports = (err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.isOperational ? err.message : 'Something went wrong';
if (process.env.NODE_ENV === 'development') {
return res.status(statusCode).json({
success: false,
message: err.message,
stack: err.stack,
});
}
res.status(statusCode).json({
success: false,
message,
});
};
Register it after all routes in app.js:
const errorHandler = require('./middleware/errorHandler');
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/posts', postRoutes);
app.use(errorHandler); // must be last
The asyncHandler Wrapper
Every async route handler needs a try/catch, or unhandled promise rejections will crash the process. Repeating try/catch in every handler is noisy. An asyncHandler wrapper removes the boilerplate:
// asyncHandler.js
const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
module.exports = asyncHandler;
Usage in a route file:
const asyncHandler = require('../utils/asyncHandler');
const AppError = require('../utils/AppError');
exports.getUser = asyncHandler(async (req, res) => {
const user = await User.findById(req.params.id);
if (!user) throw new AppError('User not found', 404);
res.status(200).json({ success: true, data: user });
});
No try/catch needed. Any thrown error is forwarded to next() and caught by the centralised handler.
Input Validation with express-validator
Accepting unvalidated user input is a security risk. The express-validator library integrates directly with Express:
npm install express-validator
const { body, validationResult } = require('express-validator');
// Validation rules as middleware
const validateUser = [
body('name')
.trim()
.notEmpty().withMessage('Name is required')
.isLength({ max: 100 }).withMessage('Name cannot exceed 100 characters'),
body('email')
.isEmail().withMessage('Please provide a valid email'),
body('password')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters'),
];
// Middleware to check results
const checkValidation = (req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(422).json({
success: false,
message: 'Validation failed',
errors: errors.array(),
});
}
next();
};
// Apply to a route
router.post('/users', validateUser, checkValidation, createUser);
Alternative: Joi
Joi lets you define a schema object separately from the route, which is useful for reuse:
const Joi = require('joi');
const userSchema = Joi.object({
name: Joi.string().max(100).required(),
email: Joi.string().email().required(),
password: Joi.string().min(8).required(),
});
const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body, { abortEarly: false });
if (error) {
return res.status(422).json({
success: false,
message: error.details.map((d) => d.message).join(', '),
});
}
next();
};
router.post('/users', validate(userSchema), createUser);
Handling 404 Routes
Add a catch-all route before the error handler to handle unknown endpoints:
app.all('*', (req, res, next) => {
next(new AppError(`Cannot find ${req.originalUrl} on this server`, 404));
});
Practice Exercise
- Create a custom
AppErrorclass withmessage,statusCode, andisOperationalproperties. - Write an
asyncHandlerwrapper and apply it to at least two route handlers. - Add
express-validatorrules to a POST route: requirename(max 100 chars),email(valid format), andage(number, min 18). - Implement a centralised error handler that shows stack traces in development but hides them in production.
Try it yourself
Key Takeaways
- Distinguish operational errors (expected failures) from programmer errors (bugs) — handle each differently.
- A custom AppError class carries both a message and an HTTP status code, making error responses consistent.
- The asyncHandler wrapper eliminates repetitive try/catch blocks by forwarding errors to Express's next() function.
- The centralised error handler middleware must be the last middleware registered — after all routes.
- Use express-validator or Joi to validate incoming data before it touches your database or business logic.
Quick Quiz
1.What is the difference between an operational error and a programmer error?
2.Why is the asyncHandler wrapper useful in Express?
3.Where should the centralised error handler middleware be registered in an Express app?
4.Which HTTP status code is most appropriate for a validation error (e.g. invalid email format)?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx