Handling Synchronous and Asynchronous Errors in Express.js

kmtenhouse

Tassa Tenhouse

Posted on December 5, 2019

Handling Synchronous and Asynchronous Errors in Express.js

Has this ever happened to you? You're cruising along, enjoying the app you just built with Node and Express -- only to run head-first into an ugly error you never expected.

Screenshot of a ReferenceError visible in the client, with a picture of a cursed emoji to emphasize how unappealing this message is

Or even worse, maybe you don't see anything at all! Maybe you've clicked on a link only to find your browser endlessly spinning. You take a peek at your backend logs, only to see those dreaded words:

UnhandledPromiseRejectionWarning: Unhandled promise rejection.
Enter fullscreen mode Exit fullscreen mode

It's time to corral the chaos: it's time to get serious about error handling :)

Step One: Synchronous Errors

By default, when a synchronous error happens in an express route handler, Express will use its built-in error handler. It will write the error to the client, leading to those stark error pages. While Express won't expose the full stack trace in production, it's still a pretty bad experience for site visitors who bump into that obnoxious "Internal Server Error".

router.get("/bad", (req, res) => {
  const a = 1; 
  res.send(a + b); //this will cause a ReferenceError because 'b' is not defined!
})
Enter fullscreen mode Exit fullscreen mode

Screenshot of an error in production: simple Internal Server Error

We can do better! Instead of leaving Express to its own devices, we can define our own custom error handler.

🛡️ Writing a custom Express error handler 🛡️

Most folks are used to writing express route handlers with 2-3 parameters: a request, a response, and optionally, a next function that can be invoked to move on to the next middleware in the chain. However, if you add a fourth parameter -- an error -- in front of the other three, this middleware becomes an error handler! When an error is thrown, the error will bypass any normal route handlers and go into the first error handler it finds downstream.

⚠️ Key point: in order to catch errors from all your routes, this error handler must be included after all route definitions! ⚠️

// First, we include all our routes:
app.use(routes);

// And at the very end, we add our custom error handler!
app.use((err, req, res, next) => {
  //(Note: it's highly recommended to add in your favorite logger here :)
  res.render("errorpage");
}
Enter fullscreen mode Exit fullscreen mode

And voila! In one fell swoop, we made that broken route show a lovely error page:
Screenshot of a styled error page

In fact, if we wanted to get extra fancy, we could even display different error pages depending on the type of error we have received! Some folks even write their own custom Error objects (extending the native class) to store information about what status code the application should send, or which page the user should be redirected to if said Error is thrown. For purposes of this demo, however, even one 'pretty' page is already light years better than the stark error we started out with.

Step Two: Asynchronous Errors

While Express will automagically catch synchronous errors and pass them to our custom error handler, asynchronous errors are a whole different beast. If a promise is rejected without being caught in an express route handler, the unhandled rejection will prevent the client from receiving any response!

Since the 'spinning vortex of doom' is a terrible fate for our site visitors, we have to ensure we always trap promise rejections and pass them along to our error handler.

➡️ Try-Catch Blocks 🛑

By wrapping our asynchronous functions in try-catch blocks, we ensure that we are always trapping rejections when they arise. As soon as a promise is rejected, the code jumps to the 'catch' block, which then passes the error on to our handler:

const alwaysRejects = function () {
  // This function creates a promise that will always reject with an error:
  return new Promise((resolve, reject) => reject(new Error("I'm stuck!")));
}

router.get("/reject", async (req, res, next) => {
  try {
    await alwaysRejects();
    res.send('Hello, World!');
  } catch (err) {
    next(err);
  }
});
Enter fullscreen mode Exit fullscreen mode

An alternative approach: Middleware for your Middleware

For an alternate method to traditional try-catch handling, The Code Barbarian recommends promisifying the route handler itself. While this option does work, it may feel a bit clunky to add a wrapper simply to avoid a try-catch.

//this function promisifies an existing express function so that any unhandled rejections within will be automagically passed to next()
function handleErr(expressFn) {
  return function (req, res, next) {
    expressFn(req, res, next).catch(next);
  };
}

const alwaysRejects = function () {
  // This function creates a promise that will always reject with an error:
  return new Promise((resolve, reject) => reject(new Error("I'm stuck!")));
}

router.get("/reject", handleErr(async (req, res, next) => {
  const result = await alwaysRejects();
  res.send('Hello, World!');
}));

Enter fullscreen mode Exit fullscreen mode

Conclusion

All in all, whichever options you pick, good error handling is here to stay!

Screenshot showing side-by-side demo of plain error vs rendered html
From old-fashioned Internal Server Error to beautiful custom error pages...the glow-up is real!

References and Credits

Express Error Handling Documentation
The Code Barbarian

💖 💪 🙅 🚩
kmtenhouse
Tassa Tenhouse

Posted on December 5, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related