Error Handling in Express async/await Route Handlers

By Hemanta Sundaray on 2022-11-17

In this blog post, we will learn how to catch errors in Express async/await route handlers.

In a typical web application, a common task of most route handlers is to perform asynchronous operations such as query a database and interact with a third-party API.

Asynchronous operations always return promises, which represent the eventual completion or failure of those operations. We consume promises either using the .then() & .catch() methods available on the Promise object or using the async/await syntax.

I prefer the async/await syntax because it is more readable and allows us to write asynchronous code that reads similarly to traditional synchronous code.

Consider the example below:

const express = require("express")

const app = express()

const fetchPosts = () => {
  return new Promise(resolve => {
    setTimeout(() => resolve("Here are the posts."), 1000)
  })
}

app.get("/posts", async (req, res) => {
  const posts = await fetchPosts()
  return res.send(posts)
})

const PORT = process.env.PORT || 5000

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`)
})

Note that we are using setTimeout() to simulate async code inside the fetchPosts() function.

Navigate to http://localhost:5000/posts and the browser prints Here are the posts. as expected. The route handler is working as expected.

However, remember that async operations can fail for all kinds of reasons. Queries to databases or third-party APIs might fail and give back errors.

Let’s make the fetchPosts() function return an error by returning a rejected Promise object with a given reason for rejection.

Replace the fetchPosts() function with the function given below and refresh the page.

const fetchPosts = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("Failed"), 2000)
  })
}

We don’t see any response and if we check the server logs, we notice an unhandled promise rejection error.

Unhandled Promise Rejection Warning

This is because express doesn’t add a .catch handler to the Promise returned by the middleware, and a response is never sent to the client.

In order to handle the rejected promise, we can use a try…catch statement:

app.get("/posts", async (req, res, next) => {
  try {
    const posts = await fetchPosts()
    return res.send(posts)
  } catch (error) {
    next(error)
  }
})

If we load the page again, we can see the error printed in the browser.

Error Message

The problem with handling errors with try…catch blocks is that we need to repeat them for every route handler. Imagine a large-scale application with hundreds of routes!

One solution is to create a middleware function that takes in the route handler and returns an async function with a try…catch statement.

const asyncErrorMiddleware = routeHandler => {
  return async (req, res, next) => {
    try {
      await routeHandler(req, res)
    } catch (err) {
      // Pass the error thrown to the Express error handling middleware
      next(err)
    }
  }
}

Then, we wrap the route handler with the middleware function.

const express = require("express")

const app = express()

const fetchPosts = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("Failed"), 2000)
  })
}

const asyncErrorMiddleware = routeHandler => {
  return async (req, res, next) => {
    try {
      await routeHandler(req, res)
    } catch (err) {
      // Pass the error thrown to the Express error handling middleware
      next(err)
    }
  }
}

app.get(
  "/posts",
  asyncErrorMiddleware(async (req, res, next) => {
    const posts = await fetchPosts()
    return res.send(posts)
  })
)

const PORT = process.env.PORT || 5000

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`)
})

Instead of writing our async error handler, we can make use of an npm package called express-async-handler. It is a middleware for handling errors inside of async express routes and passing them on to express error handlers.

Install the package using the command below:

npm install express-async-handler

Then inside server.js, we require the package in and wrap the route handler with it.

const express = require("express")
const asyncErrorHandler = require("express-async-handler")

const app = express()

const fetchPosts = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => reject("Failed"), 2000)
  })
}

app.get(
  "/posts",
  asyncErrorHandler(async (req, res, next) => {
    const posts = await fetchPosts()
    return res.send(posts)
  })
)

const PORT = process.env.PORT || 5000

app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`)
})

Now you know how to handle errors in Express async middleware.

Join the Newsletter