Express router error handler

Error Handling refers to how Express catches and processes errors that occur both synchronously and asynchronously. Express comes with a default error handler so you don’t need to write your own to get started.

Error Handling refers to how Express catches and processes errors that
occur both synchronously and asynchronously. Express comes with a default error
handler so you don’t need to write your own to get started.

Catching Errors

It’s important to ensure that Express catches all errors that occur while
running route handlers and middleware.

Errors that occur in synchronous code inside route handlers and middleware
require no extra work. If synchronous code throws an error, then Express will
catch and process it. For example:

app.get('/', (req, res) => {
  throw new Error('BROKEN') // Express will catch this on its own.
})

For errors returned from asynchronous functions invoked by route handlers
and middleware, you must pass them to the next() function, where Express will
catch and process them. For example:

app.get('/', (req, res, next) => {
  fs.readFile('/file-does-not-exist', (err, data) => {
    if (err) {
      next(err) // Pass errors to Express.
    } else {
      res.send(data)
    }
  })
})

Starting with Express 5, route handlers and middleware that return a Promise
will call next(value) automatically when they reject or throw an error.
For example:

app.get('/user/:id', async (req, res, next) => {
  const user = await getUserById(req.params.id)
  res.send(user)
})

If getUserById throws an error or rejects, next will be called with either
the thrown error or the rejected value. If no rejected value is provided, next
will be called with a default Error object provided by the Express router.

If you pass anything to the next() function (except the string 'route'),
Express regards the current request as being an error and will skip any
remaining non-error handling routing and middleware functions.

If the callback in a sequence provides no data, only errors, you can simplify
this code as follows:

app.get('/', [
  function (req, res, next) {
    fs.writeFile('/inaccessible-path', 'data', next)
  },
  function (req, res) {
    res.send('OK')
  }
])

In the above example next is provided as the callback for fs.writeFile,
which is called with or without errors. If there is no error the second
handler is executed, otherwise Express catches and processes the error.

You must catch errors that occur in asynchronous code invoked by route handlers or
middleware and pass them to Express for processing. For example:

app.get('/', (req, res, next) => {
  setTimeout(() => {
    try {
      throw new Error('BROKEN')
    } catch (err) {
      next(err)
    }
  }, 100)
})

The above example uses a try...catch block to catch errors in the
asynchronous code and pass them to Express. If the try...catch
block were omitted, Express would not catch the error since it is not part of the synchronous
handler code.

Use promises to avoid the overhead of the try...catch block or when using functions
that return promises. For example:

app.get('/', (req, res, next) => {
  Promise.resolve().then(() => {
    throw new Error('BROKEN')
  }).catch(next) // Errors will be passed to Express.
})

Since promises automatically catch both synchronous errors and rejected promises,
you can simply provide next as the final catch handler and Express will catch errors,
because the catch handler is given the error as the first argument.

You could also use a chain of handlers to rely on synchronous error
catching, by reducing the asynchronous code to something trivial. For example:

app.get('/', [
  function (req, res, next) {
    fs.readFile('/maybe-valid-file', 'utf-8', (err, data) => {
      res.locals.data = data
      next(err)
    })
  },
  function (req, res) {
    res.locals.data = res.locals.data.split(',')[1]
    res.send(res.locals.data)
  }
])

The above example has a couple of trivial statements from the readFile
call. If readFile causes an error, then it passes the error to Express, otherwise you
quickly return to the world of synchronous error handling in the next handler
in the chain. Then, the example above tries to process the data. If this fails then the
synchronous error handler will catch it. If you had done this processing inside
the readFile callback then the application might exit and the Express error
handlers would not run.

Whichever method you use, if you want Express error handlers to be called in and the
application to survive, you must ensure that Express receives the error.

The default error handler

Express comes with a built-in error handler that takes care of any errors that might be encountered in the app. This default error-handling middleware function is added at the end of the middleware function stack.

If you pass an error to next() and you do not handle it in a custom error
handler, it will be handled by the built-in error handler; the error will be
written to the client with the stack trace. The stack trace is not included
in the production environment.

Set the environment variable NODE_ENV to production, to run the app in production mode.

When an error is written, the following information is added to the
response:

  • The res.statusCode is set from err.status (or err.statusCode). If
    this value is outside the 4xx or 5xx range, it will be set to 500.
  • The res.statusMessage is set according to the status code.
  • The body will be the HTML of the status code message when in production
    environment, otherwise will be err.stack.
  • Any headers specified in an err.headers object.

If you call next() with an error after you have started writing the
response (for example, if you encounter an error while streaming the
response to the client) the Express default error handler closes the
connection and fails the request.

So when you add a custom error handler, you must delegate to
the default Express error handler, when the headers
have already been sent to the client:

function errorHandler (err, req, res, next) {
  if (res.headersSent) {
    return next(err)
  }
  res.status(500)
  res.render('error', { error: err })
}

Note that the default error handler can get triggered if you call next() with an error
in your code more than once, even if custom error handling middleware is in place.

Writing error handlers

Define error-handling middleware functions in the same way as other middleware functions,
except error-handling functions have four arguments instead of three:
(err, req, res, next). For example:

app.use((err, req, res, next) => {
  console.error(err.stack)
  res.status(500).send('Something broke!')
})

You define error-handling middleware last, after other app.use() and routes calls; for example:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use((err, req, res, next) => {
  // logic
})

Responses from within a middleware function can be in any format, such as an HTML error page, a simple message, or a JSON string.

For organizational (and higher-level framework) purposes, you can define
several error-handling middleware functions, much as you would with
regular middleware functions. For example, to define an error-handler
for requests made by using XHR and those without:

const bodyParser = require('body-parser')
const methodOverride = require('method-override')

app.use(bodyParser.urlencoded({
  extended: true
}))
app.use(bodyParser.json())
app.use(methodOverride())
app.use(logErrors)
app.use(clientErrorHandler)
app.use(errorHandler)

In this example, the generic logErrors might write request and
error information to stderr, for example:

function logErrors (err, req, res, next) {
  console.error(err.stack)
  next(err)
}

Also in this example, clientErrorHandler is defined as follows; in this case, the error is explicitly passed along to the next one.

Notice that when not calling “next” in an error-handling function, you are responsible for writing (and ending) the response. Otherwise those requests will “hang” and will not be eligible for garbage collection.

function clientErrorHandler (err, req, res, next) {
  if (req.xhr) {
    res.status(500).send({ error: 'Something failed!' })
  } else {
    next(err)
  }
}

Implement the “catch-all” errorHandler function as follows (for example):

function errorHandler (err, req, res, next) {
  res.status(500)
  res.render('error', { error: err })
}

If you have a route handler with multiple callback functions you can use the route parameter to skip to the next route handler. For example:

app.get('/a_route_behind_paywall',
  (req, res, next) => {
    if (!req.user.hasPaid) {
      // continue handling this request
      next('route')
    } else {
      next()
    }
  }, (req, res, next) => {
    PaidContent.find((err, doc) => {
      if (err) return next(err)
      res.json(doc)
    })
  })

In this example, the getPaidContent handler will be skipped but any remaining handlers in app for /a_route_behind_paywall would continue to be executed.

Calls to next() and next(err) indicate that the current handler is complete and in what state. next(err) will skip all remaining handlers in the chain except for those that are set up to handle errors as described above.

 on
April 29, 2021

A Guide to Error Handling in Express.js

Error handling often doesn’t get the attention and prioritization it deserves. Especially for newbie developers, there is more focus on setting up routing, route handlers, business logic, optimizing performance, etc. As a result, the equally (if not more) crucial error-handling part will likely be overlooked. Striving for the most optimized code and squeezing out every last ounce of performance is all well and good; yet, it’s important to remember all it takes is one unhandled error leak into your user interface to override all the seconds you helped your users save.

Because there are so many components involved in a successful, functioning web application, it is vital to foolproof your application by preparing for all possible errors and exceptions. If left mishandled, these errors can lead to a bad user experience and end up affecting your business. At the same time, errors provide critical information about potential errors in your application that could bring the whole thing down. Therefore, you must be thoughtful and intelligent about error handling in your application. 

This post will c, Node.js’s most popular server-side framework (even though most of these concepts apply to other frameworks too). Express does a great job taking care of several unhandled errors and provides an easy-to-use, flexible API that developers can utilize to build error handling middleware. 

Here’s an outline of what we’ll be covering so you can easily navigate or skip ahead in the guide:

  • How does Error Handling Work in Express.js?
    • Express Middleware Functions
    • Default Error Handling in Express.js
  • Handling Custom Errors
    • Custom Handling for Each Route
    • Writing your own Error Handling Middleware Functions
    • Adding Multiple Middleware Handlers
  • Basic Quick Tutorial: Setting up Error Handling in Express.js

How Does Error Handling Work in Express.js? 

Express.js is the most popular Javascript server-side framework, perhaps, primarily because of its ease of usage and getting started. One of the many ways it makes things easier is by automatically catching all errors in route handlers, and allowing developers to extend route handling functionalities by leveraging useful middleware functions. 

Before we see how all of this works, let’s briefly visit the concept of middleware functions in Express – most error handling functionality is achieved through these functions. 

Express Middleware Functions 

Middleware functions in Express are essentially functions that come into play after the server receives the request and before the response fires to the client. They have access to the request and the response objects. They can be used for any data processing, database querying, making API calls, sending the response, or calling the next middleware function (using the next() function). 

Two aspects of middleware functions to keep in mind are:

  • They are triggered sequentially (top to bottom) based on their sequence in code.
  • They operate until the process exits, or the response has been sent back to the client.

Let’s understand this through a small example. Below we define two middleware functions using the .use() function and one route handler (skipping the boilerplate code for the sake of simplicity):

app.use((req, res, next) => {
  console.log("Middleware 1 called.")
  console.log(req.path)
  next() // calling next middleware function or handler
})

app.get('/', (req, res) => {
  console.log("Route handler called.")
  res.send("Hello world!") // response sent back – no more middleware called
})

app.use((req, res, next) => {
  console.log("Last middleware called❓") // not called
})

Here, each time the server receives a request, the first middleware is fired, followed by the corresponding route handler (using the next() function). However, because the response returns in this handler, the last middleware function is not called. Here’s the output:

undefined
Server output

Several native as well as third-party middleware functions have been made available by the Express community and are widely for adding functionalities like session management, authentication, logging, redirecting, and so much more. This was a basic example of how middleware functions work. We will come back to them when discussing how to utilize them for error handling in our applications.

Default Error Handling in Express.js 

Express implicitly takes care of catching your errors to prevent your application from crashing when it comes to error handling. This is especially true for synchronous route handler code. Let’s see how:

Synchronous Code

Synchronous code refers to statements of code that execute sequentially and one at a time. When an error encounters synchronous code, Express catches it automatically. Here’s an example of a route handler function where we simulate an error condition by throwing an error:

app.get('/', (req, res) => {
  throw new Error("Hello error!")
})

Express catches this error for us and responds to the client with the error’s status code, message, and even the stack trace (for non-production environments).

All of this is taken care of thanks to Express’s default built-in error handler middleware function inserted at the end of your code’s middleware stack. This automatic handling saves you from bulky try/catch blocks and explicit calls to the in-built middleware (shown below) while also providing some fundamental default error handling functionality. 

app.get('/', (req, res, next) => {
  try {
      throw new Error("Hello error!")
  }
  catch (error) {
      next(error)
  }
})

You can also choose to create your own middleware function to specify your error handling logic. 

Asynchronous Code

When writing server-side code, most of your route handlers are likely using asynchronous Javascript logic to read and write files on the server, query databases, and make external API requests. Let’s see whether Express can catch errors raised from asynchronous code as well. We’ll throw an error from inside the asynchronous setTimeout() function and see what happens:

app.get('/', (req, res) => {
  setTimeout(() => {
      console.log("Async code example.")
      throw new Error("Hello Error!")
  }, 1000)
})

As you can see, our server crashed because Express didn’t handle the error for us. 

undefined
Server output

For handling errors raised during asynchronous code execution in Express (versions < 5.x), developers need to themselves catch their errors and invoke the in-built error handler middleware using the next() function. Here’s how:

app.get('/', (req, res, next) => {
  setTimeout(() => {
      try {
          console.log("Async code example.")
          throw new Error("Hello Error!")
      } catch (error) { // manually catching
          next(error) // passing to default middleware error handler
      }
  }, 1000)
})
undefined
Browser output

This is much better – we caught the error, and our server didn’t crash. This does look a little bulky because we used the setTimeout() function to demonstrate async behavior. This function does not return a promise and, therefore, can’t be chained with a quick .catch() function. However, most libraries that help with async operations return promises these days (e.g., the file system API). Below is an example of a more convenient and common way of catching errors from promises:

const fsPromises = require('fs').promises
app.get('/', (req, res, next) => {
  fsPromises.readFile('./no-such-file.txt')

     .then(data => res.send(data))

     .catch(err => next(err)) 
})

Note: Express 5.0 (currently in alpha) can automatically catch errors (and rejections) thrown by returned Promises. 

Handling Custom Errors 

Express’s default error-handling middleware is super helpful for beginners to take care of unexpected, unhandled errors. However, different developers and organizations would want their errors handled in their own way – some might want to write these to log files, others might want to alert the user or redirect them to another page, or all of the above.

Custom Handling for Each Route 

An obvious, naive way of going about this would be to define your custom error handling logic for each route handler as so:

const express = require('express')
const fsPromises = require('fs').promises;

const app = express()
const port = 3000

app.get('/one', (req, res) => {
  fsPromises.readFile('./one.txt')
    .then(data => res.send(data))
    .catch(err => { // error handling logic 1
        console.error(err) // logging error
        res.status(500).send(err)
    })
})

app.get('/two', (req, res) => {
  fsPromises.readFile('./two.txt')
    .then(data => res.send(data))
    .catch(err => { // error handling logic 2
        console.error(err)
        res.redirect('/error') // redirecting user
    })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Here, we specified two different handling logics – one for each route that attempts to read arbitrary files on the server. As you can imagine, this would get too redundant quickly and wouldn’t scale well as you add more and more routes.

Writing your Error Handling Middleware Functions 

A much better option would be to leverage Express’s middleware functions here. You could write one or more middleware functions for handling errors in your application that all of your routes could utilize by making simple next() calls. 

Middleware functions are much more convenient to work with than conventional functions because they automatically have access to the error, request, and response objects and can be invoked (or invoke others) based on their ordering using just the next() function.

You can create your own error handling middleware functions by adding the error argument to the function, apart from request, response, and next. Here is an example:

app.use((error, req, res, next) => {
  console.log("Error Handling Middleware called")
  console.log('Path: ', req.path)
  next() // (optional) invoking next middleware
})

Another thing to keep in mind is the ordering of the middleware. The error handler needs to specify middleware functions after the route handlers for the next(error) calls to be directed towards them.

Now let’s recreate the previous example, but this time with an error-handling middleware in place.

const express = require('express')
const fsPromises = require('fs').promises

const app = express()
const port = 3000

app.get('/one', (req, res, next) => {
  fsPromises.readFile('./one.txt') // arbitrary file
    .then(data => res.send(data))
    .catch(err => next(err)) // passing error to custom middleware
})

app.get('/two', (req, res, next) => {
  fsPromises.readFile('./two.txt')
    .then(data => res.send(data))
    .catch(err => {
        err.type = 'redirect' // custom prop to specify handling behaviour
        next(err)
    })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

app.use((error, req, res, next) => {
  console.log("Error Handling Middleware called")
  console.log('Path: ', req.path)
  console.error('Error: ', error)
 
  if (error.type == 'redirect')
      res.redirect('/error')

   else if (error.type == 'time-out') // arbitrary condition check
      res.status(408).send(error)
  else
      res.status(500).send(error)
})


app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
})

Instead of defining the handling behavior inside each route, we place all our logic inside the middleware. Then, based on the kind of error, we can modify the error object (or throw a custom error) and accordingly deal with it in the middleware.

This allows us to achieve the same functionality as before, but more efficiently. Assuming these files are not present on the server, if we go to /one, the server logs the error and sends back a 500 (internal server error) response. We are redirected to the /error page after the error logs if we open /two. Below are the corresponding client and server outputs:

As you can imagine, this was a fairly basic example just to give you a sense of how you can decouple your error handling logic from the route handling into a middleware function. This extends to larger applications with hundreds of routes for increased modularity, reduced redundancy, easier maintenance, and more efficient exception handling.

Adding Multiple Middleware Handlers 

In the previous section, we worked with just one middleware to handle all our errors. However, in practice, multiple middleware functions are usually employed for different aspects of error handling to have further abstractions. For example, one middleware for logging errors, another for responding to the client, perhaps another as a fail-safe catch-all handler, etc. Here’s a preview of the same based on our previous example:

// route handlers
app.get('/one')
app.get('/two') 

app.get('/error')
// middleware

app.use(errorLogger)
app.use(errorResponder)
app.use(failSafeHandler)

Let’s write the code for this.

const express = require('express')
const fsPromises = require('fs').promises

const app = express()
const port = 3000

app.get('/one', (req, res, next) => {
  fsPromises.readFile('./one.txt')
  .then(data => res.send(data))
  .catch(err => next(err)) // passing error to custom middleware
})

app.get('/two', (req, res, next) => {
  fsPromises.readFile('./two.txt')
  .then(data => res.send(data))
  .catch(err => {
      err.type = 'redirect' // adding custom property to specify handling behaviour
      next(err)
  })
})

app.get('/error', (req, res) => {
  res.send("Custom error landing page.")
})

function errorLogger(error, req, res, next) { // for logging errors
  console.error(error) // or using any fancy logging library
  next(error) // forward to next middleware
}

function errorResponder(error, req, res, next) { // responding to client
  if (error.type == 'redirect')
      res.redirect('/error')
  else if (error.type == 'time-out') // arbitrary condition check
      res.status(408).send(error)
  else
      next(error) // forwarding exceptional case to fail-safe middleware
}

function failSafeHandler(error, req, res, next) { // generic handler
  res.status(500).send(error)
}

app.use(errorLogger)
app.use(errorResponder)
app.use(failSafeHandler)

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

This allows us to achieve the same functionality as in the previous code example, but in a more modular way that would scale better as you add more routes and handle more error conditions.

However, as previously discussed, when working with multiple middleware functions, one must keep an eye on their sequence and remember that each middleware should either respond to the client or invoke the subsequent one in the stack. If the server is just left to hang, the client continues to wait. For example, if we missed using next() in the first middleware (errorLogger), the subsequent middleware functions are not invoked, and therefore, no response fires. 

Basic Quick Tutorial: Setting up Error Handling in Express.js

Now that we’ve covered almost all aspects of error handling in Express, theory-wise, let’s solidify our understanding of these concepts by creating a prototype Express application that handles errors using middleware methods in a relatively more realistic setting.

We’ll create an API that serves user posts data fetched from a dummy API (jsonplaceholder.typicode.com). We will then validate some of the posts’ properties based on some arbitrary criteria (e.g., the content length), raise custom errors if validation fails, capture these using our custom middleware, and process them accordingly. 

Step 1: Create and Setup Project 

First, create an empty folder, cd into it, generate an npm project, and install the dependencies.

mkdir my-express-app && cd my-express-app
npm init -y
npm i --save express node-fetch

Then, create files – index.js, routes.js, errors.js, and middleware.js. It is considered good practice to keep your routes, main file, and other utilities in separate files. Ideally, developers prefer different folders for better organization, but for our small prototype, just files would suffice.

undefined
Project files

Step 2: Setup the Server 

Now let’s write the code that will start our server in index.js.

// index.js
const express = require('express')
const app = express()
const port = 3000

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

We’ll start the server and make sure everything is working fine by running the node index.js command from inside the folder.

undefined
Console output

Step 3: Create Some Routes 

Now let’s create some routes in the routes.js file, and for now, just fetch some dummy JSON posts data from the dummy API (jsonplaceholder.typicode.com/posts), and serve it through our route. We will use Express’s Router module and export our routes – to import into our main index.js server file.

// routes.js
const express = require('express')
const fetch = require('node-fetch') // for making external API requests
const router = express.Router()
router.get('/', (req, res) => {
    res.send("Hello World!")
})

router.get('/user-posts', (req, res, next) => {
  fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(data => {
          console.log(data)
          res.header("Content-Type",'application/json');
          res.send(JSON.stringify(data, null, 4)) // pretty print
        })
      .catch(err => next(err)) // pass to default error handler middleware
})

router.get('/error', (req, res) => {
  res.send("The URL you are trying to reach does not exist.")
})

module.exports = router // export routes

Now let’s import these routes into our server file.

// index.js
const express = require('express')
const routes = require('./routes') // importing routes

const app = express()
const port = 3000

app.use(routes) // initializing routes

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Let’s fire up our browser and check whether the route is working.

undefined
Browser output

Our server seems to be working fine here. Let’s do some error handling now.

Step 4: Creating and Handling Custom Errors 

It is pretty helpful to create custom error classes for your applications by extending Node’s Error class in practice. These errors can raise issues specific to the application, for example – unauthorized access, unsuccessful payment, incorrect user input, etc. This allows developers to have more detailed information about the error conditions (through custom error messages and other properties), and therefore handle them better.

In our use case, let’s say we want to ensure that all the posts have a title of fewer than 100 characters and a body character count of fewer than 220 characters. If we don’t meet this condition, we want to raise a custom error message that alerts the developer about the same time. 

Now that we have the error condition in mind, let’s create our custom error classes in the errors.js file.

// errors.js
class CharacterCountExceeded extends Error { // parent error
  constructor(post_id, content) {
      super();
      this.name = this.constructor.name // good practice

      if (this instanceof LongTitleError) // checking if title or body
          this.type = 'title'
      else if (this instanceof LongBodyError)
          this.type = 'body'
 
    this.message = `The character count of post (id: ${post_id}) ${this.type} is too long. (${content.length} characters)` // detailed error message
    this.statusCode = 500 // error code for responding to client
  }
}

// extending to child error classes
class LongTitleError extends CharacterCountExceeded { }
class LongBodyError extends CharacterCountExceeded { }

module.exports = {
    CharacterCountExceeded,
    LongTitleError,
    LongBodyError
}

First, we create one parent error class (CharacterCountExceeded) for all errors that involve an exceeded character count. The constructor for this class accepts the post’s ID and the content (of the title or body) to generate the required error message and specify an error code. Then we extend this class to create two more specific children classes (LongTitleError and LongBodyError) that refer to the particular error condition.

Now we will import these into our routes.js file, check for erroneous conditions inside our route handler, and throw these custom errors wherever required.

// routes.js
const express = require('express')
const fetch = require('node-fetch')
const router = express.Router()
const { LongTitleError, LongBodyError } = require('./errors');
router.get('/', (req, res) => {
  res.send("Hello World!")
})

router.get('/user-posts', (req, res, next) => {
  fetch('https://jsonplaceholder.typicode.com/posts')
      .then(res => res.json())
      .then(posts => {
          for (post of posts) {
              if (post.title.length > 100)
                  throw new LongTitleError(post.id, post.body)
              if (post.body.length > 220)
                  throw new LongBodyError(post.id, post.body) 
          }
          console.log(posts)
          res.header("Content-Type", 'application/json')
          res.send(JSON.stringify(posts, null, 4)) // pretty print
      })
      .catch(err => next(err))
})

router.get('/error', (req, res) => {
    res.send("The URL you are trying to reach does not exist.")
})

module.exports = router

As you can see here, we traverse through all the posts, check for their title and body’s character count, and throw our custom errors accordingly. Here’s the output:

undefined

It turns out there was one post that had a body size of more than 220 characters, and we successfully captured it. At the moment, we are forwarding all our errors through the catch block to Express’s default error handler middleware. But what’s the fun in that?

Let’s create our own middleware functions and use them as we like.

Step 5: Adding Custom Error Handler Middleware

We’ll use the middleware.js file that we created before.

// middleware.js
const errorLogger = (err, req, res, next) => {
  console.error('x1b[31m', err) // adding some color to our logs
  next(err) // calling next middleware
}

const errorResponder = (err, req, res, next) => {
  res.header("Content-Type", 'application/json')
  res.status(err.statusCode).send(JSON.stringify(err, null, 4)) // pretty print
}
const invalidPathHandler = (req, res, next) => {
  res.redirect('/error')
}

module.exports = { errorLogger, errorResponder, invalidPathHandler }

Here, we add three middleware functions – one for logging errors, one for sending the error to the client, and one for redirecting a user from an invalid route to an error landing page. Now let’s import these into our main file and use them in our application.

// index.js
const express = require('express')
const routes = require('./routes')
const { errorLogger, errorResponder, invalidPathHandler } = require('./middleware')

const app = express()
const port = 3000

app.use(routes)

// middleware
app.use(errorLogger)
app.use(errorResponder)
app.use(invalidPathHandler)

app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})

Now let’s open our browser and see whether the middleware handles our LongBody error the way it’s supposed to.

undefined
Console output: Error object logged in red using the errorLogger middleware.
undefined
Server’s error response using the errorResponder middleware.
undefined
Redirecting to the error landing page upon encountering an invalid path

As you can see all our middleware functions are working as expected – in logging, responding to the client, and redirecting to the error landing page.

Wrapping it Up

We covered everything about error handling in Express.js – from default error handling of synchronous and asynchronous code to creating your own error classes and writing your own error-handling middleware functions. 

Now go ahead and make sure to handle all your errors in your Express application in a clean, non-redundant, efficient, and easy to maintain way. And if you haven’t already, write your own middleware functions and play around with native and third-party ones to explore how they can be helpful for your applications. If you are serious about your application’s performance and want to spend less time debugging issues and more time building new features, consider checking out ScoutAPM for monitoring your Node.js app’s performance and get started with a 14-day free trial.

The Express.js tagline rings true: It’s a “fast, unopinionated, minimalist web framework for Node.js.” It’s so unopinionated that, despite current JavaScript best practices prescribing the use of promises, Express.js doesn’t support promise-based route handlers by default.

With many Express.js tutorials leaving out that detail, developers often get in the habit of copying and pasting result-sending and error-handling code for each route, creating technical debt as they go. We can avoid this antipattern (and its fallout) with the technique we’ll cover today—one I’ve used successfully in apps with hundreds of routes.

Typical Architecture for Express.js Routes

Let’s start with an Express.js tutorial application with a few routes for a user model.

In real projects, we would store the related data in some database like MongoDB. But for our purposes, data storage specifics are unimportant, so we will mock them out for the sake of simplicity. What we won’t simplify is good project structure, the key to half the success of any project.

Yeoman can yield much better project skeletons in general, but for what we need, we’ll simply create a project skeleton with express-generator and remove the unnecessary parts, until we have this:

bin
  start.js
node_modules
routes
  users.js
services
  userService.js
app.js
package-lock.json
package.json

We’ve pared down the lines of the remaining files that aren’t related to our goals.

Here’s the main Express.js application file, ./app.js:

const createError  = require('http-errors');
const express = require('express');
const cookieParser = require('cookie-parser');
const usersRouter = require('./routes/users');

const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/users', usersRouter);
app.use(function(req, res, next) {
  next(createError(404));
});
app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

module.exports = app;

Here we create an Express.js app and add some basic middleware to support JSON use, URL encoding, and cookie parsing. We then add a usersRouter for /users. Finally, we specify what to do if no route is found, and how to handle errors, which we will change later.

The script to start the server itself is /bin/start.js:

const app = require('../app');
const http = require('http');

const port = process.env.PORT || '3000';

const server = http.createServer(app);
server.listen(port);

Our /package.json is also barebones:

{
  "name": "express-promises-example",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/start.js"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "express": "~4.16.1",
    "http-errors": "~1.6.3"
  }
}

Let’s use a typical user router implementation in /routes/users.js:

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  userService.getAll()
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(result => res.status(200).send(result))
    .catch(err => res.status(500).send(err));
});

module.exports = router;

It has two routes: / to get all users and /:id to get a single user by ID. It also uses /services/userService.js, which has promise-based methods to get this data:

const users = [
  {id: '1', fullName: 'User The First'},
  {id: '2', fullName: 'User The Second'}
];

const getAll = () => Promise.resolve(users);
const getById = (id) => Promise.resolve(users.find(u => u.id == id));

module.exports = {
  getById,
  getAll
};

Here we’ve avoided using an actual DB connector or ORM (e.g., Mongoose or Sequelize), simply mimicking data fetching with Promise.resolve(...).

Express.js Routing Problems

Looking at our route handlers, we see that each service call uses duplicate .then(...) and .catch(...) callbacks to send data or errors back to the client.

At first glance, this may not seem serious. Let’s add some basic real-world requirements: We’ll need to display only certain errors and omit generic 500-level errors; also, whether we apply this logic or not must be based on the environment. With that, what will it look like when our example project grows from its two routes into a real project with 200 routes?

Approach 1: Utility Functions

Maybe we would create separate utility functions to handle resolve and reject, and apply them everywhere in our Express.js routes:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err) => res.status(500).send(err);


// routes/users.js
router.get('/', function(req, res) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

router.get('/:id', function(req, res) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(err => handleError(res, err));
});

Looks better: We’re not repeating our implementation of sending data and errors. But we’ll still need to import these handlers in every route and add them to each promise passed to then() and catch().

Approach 2: Middleware

Another solution could be to use Express.js best practices around promises: Move error-sending logic into Express.js error middleware (added in app.js) and pass async errors to it using the next callback. Our basic error middleware setup would use a simple anonymous function:

app.use(function(err, req, res, next) {
  res.status(err.status || 500);
  res.send(err);
});

Express.js understands that this is for errors because the function signature has four input arguments. (It leverages the fact that every function object has a .length property describing how many parameters the function expects.)

Passing errors via next would look like this:

// some response handlers in /utils 
const handleResponse = (res, data) => res.status(200).send(data);

// routes/users.js
router.get('/', function(req, res, next) {
  userService.getAll()
    .then(data => handleResponse(res, data))
    .catch(next);
});

router.get('/:id', function(req, res, next) {
  userService.getById(req.params.id)
    .then(data => handleResponse(res, data))
    .catch(next);
});

Even using the official best practice guide, we still need our JS promises in every route handler to resolve using a handleResponse() function and reject by passing along the next function.

Let’s try to simplify that with a better approach.

Approach 3: Promise-based Middleware

One of the greatest features of JavaScript is its dynamic nature. We can add any field to any object at runtime. We’ll use that to extend Express.js result objects; Express.js middleware functions are a convenient place to do so.

Our promiseMiddleware() Function

Let’s create our promise middleware, which will give us the flexibility to structure our Express.js routes more elegantly. We’ll need a new file, /middleware/promise.js:

const handleResponse = (res, data) => res.status(200).send(data);
const handleError = (res, err = {}) => res.status(err.status || 500).send({error: err.message});


module.exports = function promiseMiddleware() {
  return (req,res,next) => {
    res.promise = (p) => {
      let promiseToResolve;
      if (p.then && p.catch) {
        promiseToResolve = p;
      } else if (typeof p === 'function') {
        promiseToResolve = Promise.resolve().then(() => p());
      } else {
        promiseToResolve = Promise.resolve(p);
      }

      return promiseToResolve
        .then((data) => handleResponse(res, data))
        .catch((e) => handleError(res, e));  
    };

    return next();
  };
}

In app.js, let’s apply our middleware to the overall Express.js app object and update the default error behavior:

const promiseMiddleware = require('./middlewares/promise');
//...
app.use(promiseMiddleware());
//...
app.use(function(req, res, next) {
  res.promise(Promise.reject(createError(404)));
});
app.use(function(err, req, res, next) {
  res.promise(Promise.reject(err));
});

Note that we do not omit our error middleware. It’s still an important error handler for all synchronous errors that may exist in our code. But instead of repeating error-sending logic, the error middleware now passes any synchronous errors to the same central handleError() function via a Promise.reject() call sent to res.promise().

This helps us handle synchronous errors like this one:

router.get('/someRoute', function(req, res){
  throw new Error('This is synchronous error!');
});

Finally, let’s use our new res.promise() in /routes/users.js:

const express = require('express');
const router = express.Router();

const userService = require('../services/userService');

router.get('/', function(req, res) {
  res.promise(userService.getAll());
});

router.get('/:id', function(req, res) {
  res.promise(() => userService.getById(req.params.id));
});

module.exports = router;

Note the different uses of .promise(): We can pass it a function or a promise. Passing functions can help you with methods that don’t have promises; .promise() sees that it’s a function and wraps it in a promise.

Where is it better to actually send errors to the client? It’s a good code-organization question. We could do that in our error middleware (because it’s supposed to work with errors) or in our promise middleware (because it already has interactions with our response object). I decided to keep all response operations in one place in our promise middleware, but it’s up to each developer to organize their own code.

Technically, res.promise() Is Optional

We’ve added res.promise(), but we’re not locked into using it: We’re free to operate with the response object directly when we need to. Let’s look at two cases where this would be useful: redirecting and stream piping.

Special Case 1: Redirecting

Suppose we want to redirect users to another URL. Let’s add a function getUserProfilePicUrl() in userService.js:

const getUserProfilePicUrl = (id) => Promise.resolve(`/img/${id}`);

And now let’s use it in our users router in async/await style with direct response manipulation:

router.get('/:id/profilePic', async function (req, res) {
  try {
    const url = await userService.getUserProfilePicUrl(req.params.id);
    res.redirect(url);
  } catch (e) {
    res.promise(Promise.reject(e));
  }
});

Note how we use async/await, perform the redirection, and (most importantly) still have one central place to pass any error because we used res.promise() for error handling.

Special Case 2: Stream Piping

Like our profile picture route, piping a stream is another situation where we need to manipulate the response object directly.

To handle requests to the URL we’re now redirecting to, let’s add a route that returns some generic picture.

First we should add profilePic.jpg in a new /assets/img subfolder. (In a real project we would use cloud storage like AWS S3, but the piping mechanism would be the same.)

Let’s pipe this image in response to /img/profilePic/:id requests. We need to create a new router for that in /routes/img.js:

const express = require('express');
const router = express.Router();

const fs = require('fs');
const path = require('path');

router.get('/:id', function(req, res) {
  /* Note that we create a path to the file based on the current working
   * directory, not the router file location.
   */

  const fileStream = fs.createReadStream(
    path.join(process.cwd(), './assets/img/profilePic.png')
  );
  fileStream.pipe(res);
});

module.exports = router;

Then we add our new /img router in app.js:

app.use('/users', require('./routes/users'));
app.use('/img', require('./routes/img'));

One difference likely stands out compared to the redirect case: We haven’t used res.promise() in the /img router! This is because the behavior of an already-piped response object being passed an error will be different than if the error occurs in the middle of the stream.

Express.js developers need to pay attention when working with streams in Express.js applications, handling errors differently depending on when they occur. We need to handle errors before piping (res.promise() can help us there) as well as midstream (based on the .on('error') handler), but further details are beyond the scope of this article.

Enhancing res.promise()

As with calling res.promise(), we’re not locked into implementing it the way we have either. promiseMiddleware.js can be augmented to accept some options in res.promise() to allow callers to specify response status codes, content type, or anything else a project might require. It’s up to developers to shape their tools and organize their code so that it best suits their needs.

Express.js Error Handling Meets Modern Promise-based Coding

The approach presented here allows for more elegant route handlers than we started with and a single point of processing results and errors—even those fired outside of res.promise(...)—thanks to error handling in app.js. Still, we are not forced to use it and can process edge cases as we want.

The full code from these examples is available on GitHub. From there, developers can add custom logic as needed to the handleResponse() function, such as changing the response status to 204 instead of 200 if no data is available.

However, the added control over errors is much more useful. This approach helped me concisely implement these features in production:

  • Format all errors consistently as {error: {message}}
  • Send a generic message if no status is provided or pass along a given message otherwise
  • If the environment is dev (or test, etc.), populate the error.stack field
  • Handle database index errors (i.e., some entity with a unique-indexed field already exists) and gracefully respond with meaningful user errors

This Express.js route logic was all in one place, without touching any service—a decoupling that left the code much easier to maintain and extend. This is how simple—but elegant—solutions can drastically improve project structure.


Further Reading on the Toptal Engineering Blog:

  • How to Build a Node.js Error-handling System

Understanding the basics

  • What is Express middleware?

    Express.js middleware functions are functions that have access to the request object (typically «req»), the response object («res»), and the next middleware function in the application’s request-response cycle («next»). They can add additional logic before or after route handler execution.

  • What are routes in Express.js?

    An Express.js route is a handler function corresponding to a given type of HTTP event matching a specified URI pattern. It’s sent to an Express.js router or Express.js app object and contains logic about processing the HTTP request and sending results back to the client.

  • What is an Express.js router?

    An Express.js router is a class where each of its instances is an isolated set of middleware functions and routes. It’s a sort of «mini-application,» capable only of performing middleware and routing functions. Every Express.js application has a built-in app router.

  • What is error handling in Express.js?

    Error handling in Express.js is a technique to handle errors in different places by passing them to a single error handler. The error handler then performs common logic on errors, like sending them in a response to the client.

  • How does a promise work in JavaScript?

    The Promise object built into JavaScript represents asynchronous operation. It can be in one of three states: pending, fulfilled, or rejected. Additional actions on fulfilled and rejected results may be applied using handlers passed to the object’s then() and catch() methods, respectively.

  • Why do we use promises in JavaScript?

    We use promises in JavaScript to avoid «callback hell»—a code structure whereby every asynchronous result handler creates an additional nesting layer.

Handling Errors

Express.js makes it a breeze to handle errors in your routes.

Express lets you centralizes your error-handling through middleware.

Let’s look at patterns for how to get the most out of your error-handling.

First, our error-handling middleware looks like this:

app.use(function(err, req, res, next) {
  console.error(err.message); // Log error message in our server's console
  if (!err.statusCode) err.statusCode = 500; // If err has no specified error code, set error code to 'Internal Server Error (500)'
  res.status(err.statusCode).send(err.message); // All HTTP requests must have a response, so let's send back an error with its status code and message
});

Express.js interprets any route callback with four parameters as error-handling middleware. Our first parameter is err. You should put error-handling middleware at the end of your routes and middleware in your application. This let’s you catch any thrown errors.

Upstream, we can follow a simple pattern to help us understand and identify errors. Let’s look at some examples.

...

app.get('/forbidden', function(req, res, next) {
  let err = new Error(`${req.ip} tried to access /Forbidden`); // Sets error message, includes the requester's ip address!
  err.statusCode = 403;
  next(err);
});

...

Here’s how we can handle a forbidden request. To use our error-handling middleware we need two things.

First, we need to throw or create an Error. When we create our error, we can define a .message property by passing in a string as a parameter. This becomes our err.message, used later. Now we can attach a status code appropriate to the problem. We should give Forbidden requests the 403 status code.

The last and most important step is to pass a parameter into next(). If next receives an argument, Express will assume there was an error, skip all other routes, and send whatever was passed to any error-handling middleware you have defined.

Our next(err) is like saying ‘This is definitely an error. Go straight to error handling!’

Our middleware can now use .message and .statusCode to log, handle, and communicate the specifics of our error.

Generalized Error Handling

Obviously, we can’t specify routes like /forbidden for every possible broken or wrong route in our application.

How can we handle a broader set of errors?

Using the wildcard ‘*’ in our route, we can create a route to catch every request to a route we have not defined elsewhere.

...

app.get('*', function(req, res, next) {
  let err = new Error('Page Not Found');
  err.statusCode = 404;
  next(err);
});

...

This route will grab anything that isn’t routed elsewhere and throw a generic ‘404’ error.

But we can do even better.

Let’s refactor our route to make it tell Express that users shouldn’t just receive an error, but should be redirected to an error page.

...

app.get('*', function(req, res, next) {
  let err = new Error(`${req.ip} tried to reach ${req.originalUrl}`); // Tells us which IP tried to reach a particular URL
  err.statusCode = 404;
  err.shouldRedirect = true; //New property on err so that our middleware will redirect
  next(err);
});

...

Let’s spice things up a bit in our middleware…

...

app.use(function(err, req, res, next) {
  console.error(err.message);
  if (!err.statusCode) err.statusCode = 500; // Sets a generic server error status code if none is part of the err

  if (err.shouldRedirect) {
    res.render('myErrorPage') // Renders a myErrorPage.html for the user
  } else {
    res.status(err.statusCode).send(err.message); // If shouldRedirect is not defined in our error, sends our original err data
  }
});

...

Now our application will send any requests to nonsensical routes directly to an error page. On our server, we’ll still see error data.

That’s it. Remember:

  1. throw your Error with a useful message
  2. pass a parameter into next()
  3. centralize your errors in error-handling middleware!

Good luck!

Error handling is a crucial part of any production-ready application. It’s often neglected when you are exploring project ideas and trying to learn. But you should never forget about it when you launch your project for public use.

Your users deserve to have the best experience and receive useful error messages. And you deserve the peace of mind that your application can handle all kinds of errors.

When you decide to handle errors yourself, you can add useful information to them, such as HTTP response status codes. You can separate critical errors from those caused by users. Doing error handling yourself gives you options like error logging or sending yourself an email.

In this post you will learn about the many parts of error handling, such as the following:

  • catching all types of errors
  • funneling all errors into a single error handler
  • creating your own error-handling middleware
  • setting up a custom error class
  • implementing the error handler to process all errors

To just see the code, visit this demo repository.

Prerequisites

You should have an Express application set up with TypeScript.

These are the dependencies used in this post.

npm i express
npm i @types/express @types/node ts-node-dev typescript --save-dev

Catching Errors

To handle errors, you first must catch them. You could program the best error handler, but it wouldn’t matter if some errors would escape it.

Express can catch all synchronous errors and send them to its error-handling middleware.

To verify, try throwing an error in one of your routes.

router.get('/', (req: Request, res: Response) => {
  throw new Error('This is an error');

  res.json({ status: 'ok' });
});

Visit the / route in your browser and you should see an error.

Error: This is an error
    at /path/to/project/src/routes.ts:6:9
    ...

Express includes the stack trace in the error message you see in your browser. If you set Node environment to production (NODE_ENV=production), Express will hide the stack trace.

However, errors thrown in asynchronous code can go unnoticed by Express.

Catching Errors in Asynchronous Code

As just mentioned, Express doesn’t catch errors thrown in asynchronous code. Unless you are from the future and using Express 5. You can skip this section then.

For those stuck with Express 4, consider the following code that throws an error inside a promise. Something that could happen when reading user data from a database.

const getUserFromDb = () => {
  return new Promise(() => {
    throw new Error('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  const data = await getUserFromDb();

  res.json({ user: data });
});

If you revisit the / route, the page won’t load and you will need to restart your application.

You need to catch errors in async code yourself. Otherwise Express won’t pass them to the default error-handling middleware. You can use try/catch and the NextFunction to do that.

import { NextFunction, Request, Response } from 'express';

router.get('/', async (req: Request, res: Response, next: NextFunction) => {
  try {
    const data = await getUserFromDb();

    res.json({ user: data });
  } catch (error) {
    next(error);
  }
});

Notice how you pass the caught error to next function. Any argument you pass to next, Express will treat as an error. By calling next(error), you jump from any middleware straight to the error-handling middleware.

The problem with try/catch approach is having to do it tens or hundreds of times as your app grows. It is repetitive, hence error-prone.

You can use a package dedicated to catching async errors for you. One such package is express-async-errors.

Install it as a dependency.

npm i express-async-errors

And import it before you register any router handlers.

// src/routes.ts

import 'express-async-errors';
import { NextFunction, Request, Response, Router } from 'express';

const router = Router();

const getUserFromDb = () => {
  return new Promise(() => {
    throw new Error('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  const data = await getUserFromDb();

  res.json({ user: data });
});

export default router;

The express-async-errors package will make sure async errors in your routes get caught. And Express will be able to handle these errors.

So far you have learned that Express can catch errors in synchronous code and how to help it catch async errors. What about errors that Express doesn’t notice? You need to deal with unhandled rejections and uncaught exceptions yourself.

Dealing With Unhandled and Uncaught Errors

Sometimes an error goes unnoticed and Express doesn’t handle it. When Node.js encounters such errors, it emits events that you can listen to.

The first event you need to listen to is unhandledRejection. It happens when your code doesn’t handle a rejected Promise with a .catch().

const getUserFromDb = async () => {
  return new Promise((_, reject) => {
    reject('This is an error');
  });
};

router.get('/', async (req: Request, res: Response) => {
  getUserFromDb()
    .then(value => {
      res.json(value);
    })
});

Since this code doesn’t handle rejections from getUserFromDb with a .catch(), an UnhandledPromiseRejection warning shows up in Node.js console.

To fix it, you can register a listener function on the process object that handles the unhandledRejection event.

// src/process.ts

process.on('unhandledRejection', (reason: Error | any) => {
  console.log(`Unhandled Rejection: ${reason.message || reason}`);

  throw new Error(reason.message || reason);
});

Since rejections are used to return errors from Promises, you can throw an error with the given reason from the rejection.

In this case, Express won’t handle the thrown error anyway. You have created an uncaught exception. Any errors in your code base, that Express can’t handle, turn into uncaught exceptions. Luckily, you can catch them errors yourself.

To catch uncaught exceptions, listen to the uncaughtException event by setting up an event listener on the process object.

// src/process.ts

process.on('uncaughtException', (error: Error) => {
  console.log(`Uncaught Exception: ${error.message}`);

  errorHandler.handleError(error);
});

Here you want to funnel the errors into a function that handles them. Later in this post you will learn how to implement the handleError function.

You can crash your application by calling process.exit(1). Set up an automatic restart mechanism when your application exits with a non-zero code.

Don’t forget to import the code that registers the process event listener functions into your app.

// index.ts

import express, { Application } from 'express';
import './src/process';

You learned how to funnel uncaught errors into your own error handler. The errors caught in middleware functions are still handled by Express. If you want to handle them yourself, you need to create your own error-handling middleware.

Creating a Custom Error-Handling Middleware

To override default Express error responses, you need to create your own error-handling middleware.

An error-handling middleware differs from other middleware functions by having 4 parameters — err, req, res, next. Additionally, it has to be the last middleware you set up in your application. For this reason, call app.use(router) after all other app.use() calls in your index.ts. And register the error-handling middleware after registering other route handlers.

Now go ahead and register your custom error-handling middleware.

// src/routes.ts

import { NextFunction, Request, Response, Router } from 'express';
import { errorHandler } from './exceptions/ErrorHandler';

// ... all other routes

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  errorHandler.handleError(err, res);
});

Similarly to how you previously handled uncaught exceptions, you can funnel all errors into errorHandler, which you will get to implement very soon. You should also pass the Response object to the error handler so you can use it to send a response.

You can have more than one error-handling middleware. To pass the error from one to another, call the next function and pass it the error.

// ... all other routes

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 1. Log the error or send it to a 3rd party error monitoring software
  logger.logError(err);

  next(err);
});

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 2. Send an email to yourself, or a message somewhere
  messenger.sendErrorMessage(err);

  next(err);
});

router.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  // 3. Lastly, handle the error
  errorHandler.handleError(err, res);
});

The error will flow through the error-handling middleware functions from first to last (1-2-3).

The last error-handling middleware should send a response, so the client’s connection doesn’t hang up. In this case, handleError should send a response through the res argument.

You can also call next(err) in your last error-handling middleware to send it to Express error handler.

Good job, you have learned a lot. You have set up an error-handling middleware that funnels the caught errors into your error handler. Funneling both uncaught and caught errors, you are almost ready to handle them. One last thing you should do is create a custom error class. This will help to determine the severity of an error and what the HTTP response status code should be.

Creating Custom Error Class

You can use a custom error class to differentiate errors from one another. You might want to add data to an error or handle it differently than other types of errors.

One improvement you can do is attaching an HTTP response status code to your errors.

You can use an enum to map status codes to a human readable name. This way you don’t have to remember the numbers.

// exceptions/AppError.ts

export enum HttpCode {
  OK = 200,
  NO_CONTENT = 204,
  BAD_REQUEST = 400,
  UNAUTHORIZED = 401,
  NOT_FOUND = 404,
  INTERNAL_SERVER_ERROR = 500,
}

These are some of the most common status codes. Go ahead and add any other ones your application uses. You can use this MDN article as a reference.

Another improvement is separating critical application errors from those that are expected. For example, validation errors caused by user input are fine. Errors thrown from mistakes made by you, the developer, are bad.

Now go and create your own AppError class that extends the default Error class.

// exceptions/AppError.ts

export enum HttpCode { /*...*/ }

interface AppErrorArgs {
  name?: string;
  httpCode: HttpCode;
  description: string;
  isOperational?: boolean;
}

export class AppError extends Error {
  public readonly name: string;
  public readonly httpCode: HttpCode;
  public readonly isOperational: boolean = true;

  constructor(args: AppErrorArgs) {
    super(args.description);

    Object.setPrototypeOf(this, new.target.prototype);

    this.name = args.name || 'Error';
    this.httpCode = args.httpCode;

    if (args.isOperational !== undefined) {
      this.isOperational = args.isOperational;
    }

    Error.captureStackTrace(this);
  }
}

When you use super in a child class, you call the constructor of parent class. In this case, calling super will trigger Error class constructor, which sets the error’s message property to contain your description.

The required httpCode and description is what your application will return in responses. Optionally, you can give your error a name.

The isOperational property is what determines if this error is a serious mistake. Setting it to true means that the error is normal and the user should receive an explanation what caused it.

Any error that is not operational indicates a problem with your code, which you should investigate and fix.

Using the Custom Error Class

You use your AppError class whenever you want to fail the client’s request. Whether it be because user lacks permissions, their input is invalid, or they are not logged in. Or for any other reason you desire.

import { AppError, HttpCode } from './exceptions/AppError';

router.get('/user/:id', async (req: Request, res: Response) => {
  if (!res.locals.user) {
    throw new AppError({
      httpCode: HttpCode.UNAUTHORIZED,
      description: 'You must be logged in',
    });
  }

  const user = await getUserFromDb();

  if (!user) {
    throw new AppError({
      httpCode: HttpCode.NOT_FOUND,
      description: 'User you are looking for does not exist',
    });
  }

  res.json(user);
});

You are not limited to routes, you can throw AppError anywhere in your code. Remember to set isOperational to false when throwing a critical error.

Your error-handling middleware that you created earlier will catch all errors from your routes. It will then send them to your error handler, which you are now finally going to create.

Creating an Error Handler

Your error handler should distinguish errors that can be trusted. A trusted error doesn’t take much work, you just have to send an error response to the client. On the other hand, an error you can’t trust requires extra steps.

Start by creating an ErrorHandler class that can determine if an error can be trusted.

// exceptions/ErrorHandler.ts

import { Response } from 'express';
import { AppError, HttpCode } from './AppError';

class ErrorHandler {
  private isTrustedError(error: Error): boolean {
    if (error instanceof AppError) {
      return error.isOperational;
    }

    return false;
  }
}

export const errorHandler = new ErrorHandler();

You can’t trust any other error than your custom AppError. On top of that, if you set isOperational to false when throwing an AppError, you can’t trust such error either.

Now you should handle trustworthy errors coming into your error handler separately from the rest. Check if the error is trustworthy and send it to its dedicated function. Otherwise send it to a function for critical errors.

class ErrorHandler {
  private isTrustedError(error: Error): boolean { /* ...  */ }

  public handleError(error: Error | AppError, response?: Response): void {
    if (this.isTrustedError(error) && response) {
      this.handleTrustedError(error as AppError, response);
    } else {
      this.handleCriticalError(error, response);
    }
  }
}

If an error is trustworthy, it is an instance of AppError, so you can pass handleTrustedError an argument of error as AppError. Since trusted errors can only come from your error-handling middleware, your error handler always receives the Response object along with the error. So, pass it to handleTrustedError as well.

In case of untrustworthy errors, you will check if the response is defined. Because these errors can come from outside the request-response cycle. For example, when you handle an uncaught exception.

Handle a trusted error by sending the client a response with the HTTP status code and a message.

class ErrorHandler {
  // ...

  private handleTrustedError(error: AppError, response: Response): void {
    response.status(error.httpCode).json({ message: error.message });
  }
}

This is where you could also pass the name property to json(). Naming errors can be useful if you want to translate the error message on the client.

On the other hand, untrustworthy errors are dangerous, because they can make your application behave unexpectedly. Based on Node.js best practices on error handling, you should crash your application when you catch such error.

class ErrorHandler {
  // ...

  private handleCriticalError(error: Error | AppError, response?: Response): void {
    if (response) {
      response
        .status(HttpCode.INTERNAL_SERVER_ERROR)
        .json({ message: 'Internal server error' });
    }

    console.log('Application encountered a critical error. Exiting');
    process.exit(1);
  }
}

Since untrustworthy errors come from outside the error-handling middleware, you should check if Response is available. If it’s defined, send a generic server error message to the client.

This is where you would benefit from an automatic restart setup. If your application exits with a non-zero code, it should restart by itself, so you can crash on critical errors without much downtime.

I have written a post Graceful Shutdown in Express that explains how to create your own exit handler.

Of course, you can do much more than just exiting your application. You can set up error logging or notify yourself via email or a messaging service. You could also improve the shutdown procedure to be graceful, stopping all HTTP connections before the exit.

Summary

You are now knowledgeable of what goes into error handling in Express. Here’s a recap of the things you’ve learned:

  • You need to use express-async-errors, because Express can’t catch async errors on its own.
  • You can catch unhandled rejections and uncaught exceptions by listening to events with process.on.
  • You can customize error handling by making your own error-handling middleware.
  • You should use custom error class to save more information, such as status codes and trustworthiness of the error.
  • You should treat untrustworthy errors seriously and probably just restart your application.

You can see the code in this demo repository.

Понравилась статья? Поделить с друзьями:
  • Express gate error code 8c000002 что это
  • Express error type
  • Express error page
  • Express error handler typescript
  • Express error handler middleware