In this post, we see three ways of handling errors in an ExpressJS Server:
Error handling in ExpressJS can easily be done on the spot by setting the response status and sending an error message or maybe a more structured json error:
app.get('/categories/:categoryId', (req, res) => {
Category.findById(req.params.categoryId, (err, category) => {
if (err) {
res.status(500).send('Something broke!');
return;
}
if (!category) {
res.status(404).json({ error: 'Not Found', message: 'Could not find Category of id ' + categoryId });
} else {
res.status(200).json(category);
}
});
});
A better way to handle this would be to use a set of error handling middlewares:
app.use((err, req, res, next) => {
Logger.log(err);
next(err)
});
app.use((err, req, res, next) => {
if (err instanceof AssertionError) {
return res.status(400).json({
type: 'AssertionError',
message: err.message
});
}
next(err);
});
app.use((err, req, res, next) => {
if (err instanceof MongoError) {
return res.status(503).json({
type: 'MongoError',
message: err.message
});
}
next(err);
});
[...]
app.use((err, req, res, next) => {
// Hide error details from end user
return res.status(500).json(error: 'Internal Server Error', message: 'This error has been logged and sent to the Development team to improve our application! Sorry for any inconvenience...');
});
Remember that routes and middlewares are run in order. These error handling middlewares should be defined after all your other routes and middlewares. Our first middleware is logging all errors for further review by the Development Team. And the last middleware should be the default catch-them-all error handling middleware.
Our route above becomes:
app.get('/categories/:categoryId', (req, res) => {
Category.findById(req.params.categoryId, function (err, category) {
if (err) {
next(err);
}
if (!category) {
next(new NotFoundError('Not Found: Could not find Category of id ' + categoryId));
} else {
res.status(200).json(category);
}
});
});
Getting better, but defining all these Error Handling Middleware is a tedious process. We will also have to define NotFoundError and others Errors... It feels like we are reinventing the wheel...
Boom Node Module provides HTTP-friendly error objects that will make our Error Handling much easier!
Boom defines a bunch of HTTP Errors:
Yes, even the Teapot, error! 😂
Boom.teapot('sorry, no coffee...');
{
"statusCode": 418,
"error": "I'm a Teapot",
"message": "Sorry, no coffee..."
}
Our error handling middlewares becomes:
app.use((err, req, res, next) => {
Logger.log(err);
next(err)
});
app.use((err, req, res, next) => {
return res.status(err.output.statusCode).json(err.output.payload);
});
And our route becomes:
app.get('/categories/:categoryId', (req, res) => {
if (!mongoose.Types.ObjectId.isValid(categoryId)) {
return next(boom.badRequest("categoryId is not a valid ObjectId: " + categoryId));
}
Category.findById(categoryId, function (err, category) {
if (err) {
return next(boom.boomify(err, { message: "Category not found with id " + categoryId }));
}
if (!category) {
return next(boom.notFound("Category not found with id " + categoryId));
}
req.category = category;
return next();
});
});
Easy!!!
And, another benefit of using Boom is that all Server Errors (Status Code >= 500) messages are hidden from the end user. Instead, it will generate the following payload:
{
"statusCode": 500,
"error": "Internal Server Error",
"message": "An internal server error occurred"
}
You can still access the real error message for logging purpose but the output is sanitized.