How do you design custom error classes and propagate errors in Express?
Quick Answer
Extend the built-in Error class to add fields like statusCode and isOperational, so handlers can respond appropriately. In Express, throw/next these errors and centralize handling in a single error-handling middleware (four arguments) that maps them to HTTP responses.
Detailed Answer
Answer:
Custom error classes carry structured context beyond a message:
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = true; // distinguishes expected errors from bugs
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource) { super(`${resource} not found`, 404); }
}
class ValidationError extends AppError {
constructor(msg) { super(msg, 400); }
}
Propagating in Express:
app.get('/users/:id', async (req, res, next) => {
try {
const user = await db.getUser(req.params.id);
if (!user) throw new NotFoundError('User');
res.json(user);
} catch (err) {
next(err); // hand off to the error middleware
}
});
(An asyncHandler wrapper can remove the repetitive try/catch by catching rejections and calling next for you. Express 5 forwards async rejections automatically.)
Centralized error-handling middleware — note the four parameters (that's how Express recognizes it), registered last:
app.use((err, req, res, next) => {
const status = err.statusCode || 500;
// Log full details server-side; return a safe message to the client
if (!err.isOperational) logger.error(err);
res.status(status).json({ error: err.message });
});
Benefits: one place to format responses, log, and hide internal details; handlers stay focused on the happy path; the isOperational flag lets you decide what to expose vs. treat as a crash.