Last weekend I had the pleasure to visit Tokyo and gave a talk at the amazing nodefest conference, titled Node.js Production Checklist. The talk was dedicated to topics like error handling, monitoring and logging best practices, as well as incident handling. If you interested in all the slides, you can find it on Speakerdeck.
In this blog post you will learn about how you can handle errors in Express applications.
One of the most crucial things to get right when building web applications are error handling. As the system will depend on other services, databases, as well as consumers of those services, the unexpected becomes expected. Databases will fail, services become unavailable and your consumers won't be calling your endpoints with the parameters you expect.
To prepare your application for such circumstances, you have to handle errors properly. Let's take a look at the following endpoint:
app.get("/users/:id", (req, res) => {const userId = req.params.id;if (!userId) {return res.sendStatus(400).json({error: "Missing id",});}Users.get(userId, (err, user) => {if (err) {return res.sendStatus(500).json(err);}res.send(users);});});
The endpoint above has some serious flaws:
Let's see how you can fix these issues! 👌
All Express route handlers come with a third function parameter, next
, which can be used to call the next middleware, or to signal Express that an error happened:
app.get("/users/:id", (req, res, next) => {const userId = req.params.id;if (!userId) {const error = new Error("missing id");error.httpStatusCode = 400;return next(error);}Users.get(userId, (err, user) => {if (err) {err.httpStatusCode = 500;return next(err);}res.send(users);});});
This way you've already let Express know that an error arose, and the error handler will get called. To add an error handler to your application, you have to add a middleware with four parameters - that's how Express will know it is an error handler.
app.use((err, req, res, next) => {// log the error...res.sendStatus(err.httpStatusCode).json(err);});
Even if this solution works, it adds a lot of boilerplate code - let's get rid of that!
boom is an HTTP-friendly error object, that provides you helpers for standard HTTP errors:
const boom = require("boom");app.get("/users/:id", (req, res, next) => {const userId = req.params.id;if (!userId) {return next(boom.badRequest("missing id"));}Users.get(userId, (err, user) => {if (err) {return next(boom.badImplementation(err));}res.send(users);});});
To make sure we return with the newly created status codes and errors, you have to update the error handler to use properties from boom errors:
app.use((err, req, res, next) => {if (err.isServer) {// log the error...// probably you don't want to log unauthorized access// or do you?}return res.status(err.output.statusCode).json(err.output.payload);});
If you'd like to use async-await in your Express route handlers, you have to do the following:
next
in your route handlers, simply throw.const boom = require("boom");// wrapper for our async route handlers// probably you want to move it to a new fileconst asyncMiddleware = (fn) => (req, res, next) => {Promise.resolve(fn(req, res, next)).catch((err) => {if (!err.isBoom) {return next(boom.badImplementation(err));}next(err);});};// the async route handlerapp.get("/users/:id",asyncMiddleware(async (req, res) => {const userId = req.params.id;if (!userId) {throw boom.badRequest("missing id");}const user = await Users.get(userId);res.json(user);}));