Error Handling in Express

When I first started working with Express and tried to figure out how to handle errors, I had a hard time. It seemed as if no one wrote about what I needed. As a result, I had to look for the answers to my questions myself. Today I want to tell everything I know about error handling in Express applications. Let's start with synchronous errors.







Synchronous Error Handling



If you need to handle a synchronous error, then you can, first of all, using the throw



statement, throw



such an error in the Express request handler. Please note that request handlers are also called “controllers”, but I prefer to use the term “request handler” as it seems to me more clearly.



Here's what it looks like:



 app.post('/testing', (req, res) => {  throw new Error('Something broke! ') })
      
      





Such errors can be caught using the Express error handler. If you did not write your own error handler (we will talk more about this below), then Express will handle the error using the default handler.



Here's what the standard Express error handler does:



  1. Sets the HTTP response status code to 500.
  2. Sends a text response to the entity executing the request.
  3. Logs a text response to the console.








Error message displayed in console



Asynchronous Error Handling



To handle asynchronous errors, you need to send an error to the Express error handler via the next



argument:



 app.post('/testing', async (req, res, next) => {  return next(new Error('Something broke again! ')) })
      
      





This is what gets to the console when logging this error.









Error message displayed in console



If you use the async / await construct in an Express application, then you need to use a wrapper function like express-async-handler . This allows you to write asynchronous code without try / catch blocks. Read more about async / await in Express here .



 const asyncHandler = require('express-async-handler') app.post('/testing', asyncHandler(async (req, res, next) => {  //  - }))
      
      





After the request handler is wrapped in express-async-handler



, you can, as described above, throw an error using the throw



instruction. This error will go to the Express error handler.



 app.post('/testing', asyncHandler(async (req, res, next) => {  throw new Error('Something broke yet again! ') }))
      
      











Error message displayed in console



Writing your own error handler



Express error handlers take 4 arguments:



  1. error
  2. req
  3. res
  4. next


You need to place them after intermediate handlers and routes.



 app.use(/*...*/) app.get(/*...*/) app.post(/*...*/) app.put(/*...*/) app.delete(/*...*/) //           app.use((error, req, res, next) => { /* ... */ })
      
      





If you create your own error handler, Express will stop using the standard error handler. In order to handle the error, you need to generate an answer for the front-end application that addressed the endpoint at which the error occurred. This means that you need to do the following:



  1. Generate and send a suitable response status code.
  2. Form and send a suitable answer.


Which status code is appropriate in each particular case depends on what exactly happened. Here is a list of common errors that you should be prepared to handle:



  1. Error 400 Bad Request



    . Used in two situations. Firstly, when the user did not include the required field in the request (for example, the field with credit card information was not filled in the payment form sent). Secondly, when the request contains incorrect data (for example, entering different passwords in the password field and in the password confirmation field).
  2. Error 401 Unauthorized



    . This response status code is applied if the user has entered incorrect credentials (such as username, email address or password).
  3. Error 403 Forbidden



    . Used when the user is not allowed access to the endpoint.
  4. Error 404 Not Found



    . It is used in cases where the endpoint cannot be detected.
  5. Error 500 Internal Server Error



    . It is applied when the request sent by the front-end is generated correctly, but some error occurred on the backend.


After the appropriate response status code is defined, it must be set using res.status



:



 app.use((error, req, res, next) => {  // ,         res.status(400)  res.json(/* ... */) })
      
      





The response status code should correspond to the error message. To do this, send the status code along with the error.



The easiest way to do this is with the http-errors package. It allows sending three pieces of information in error:



  1. The response status code.
  2. The message associated with the error.
  3. Any data that needs to be sent (this is optional).


Here's how to install the http-errors



package:



 npm install http-errors --save
      
      





Here's how to use this package:



 const createError = require('http-errors') //   throw createError(status, message, properties)
      
      





Consider an example that will allow you to properly understand this all.



Imagine that we are trying to find a user at his email address. But this user cannot be found. As a result, we decide to send a User not found



error in response to the corresponding request, informing the caller that the user was not found.



Here is what we will need to do when creating the error:



  1. Set the response status code to 400 Bad Request



    (after all, the user entered incorrect data). This will be our first parameter.
  2. Send a message to the caller like User not found



    . This will be the second parameter.


 app.put('/testing', asyncHandler(async (req, res) => {  const { email } = req.body  const user = await User.findOne({ email })  //     -    if (!user) throw createError(400, `User '${email}' not found`) }))
      
      





You can get the status code using the error.status



construct, and the error message using error.message



:



 //   app.use((error, req, res, next) => {  console.log('Error status: ', error.status)  console.log('Message: ', error.message) })
      
      











The result of logging errors in the console



Then the response state is set using res.status



, and the message is written to res.json



:



 app.use((error, req, res, next) => {  //      res.status(error.status)  //    res.json({ message: error.message }) })
      
      





Personally, I prefer to send a status code, message, and stack trace result in such responses. This makes debugging easier.



 app.use((error, req, res, next) => {  //      res.status(error.status)  //    res.json({    status: error.status,    message: error.message,    stack: error.stack  }) })
      
      





▍ Default response status code



If the source of the error is not createError



, then it will not have the status



property. Here is an example in which an attempt was made to read a non-existent file using fs.readFile



:



 const fs = require('fs') const util = require('util') //  readFile  ,  ,  async/await-. //     : https://zellwk.com/blog/callbacks-to-promises const readFilePromise = util.promisify(fs.readFile) app.get('/testing', asyncHandler(async (req, res, next) => {  const data = await readFilePromise('some-file') })
      
      





Such an error object will not have the status



property:



 app.use((error, req, res, next) => {  console.log('Error status: ', error.status)  console.log('Message: ', error.message) })
      
      











The result of logging errors in the console



In such cases, you can set the default error code. Namely, we are talking about the 500 Internal Server Error



:



 app.use((error, req, res, next) => {  res.status(error.status || 500)  res.json({    status: error.status,    message: error.message,    stack: error.stack  }) })
      
      





▍Changing the error status code



Suppose we are going to read a certain file using the data provided by the user. If such a file does not exist, it means that we need to give a 400 Bad Request



error. After all, the server cannot be found because the file cannot be found.



In this case, you need to use the try / catch construct to catch the original error. Then you need to recreate the error object using createError



:



 app.get('/testing', asyncHandler(async (req, res, next) => {  try {    const { file } = req.body    const contents = await readFilePromise(path.join(__dirname, file))  } catch (error) {    throw createError(400, `File ${file} does not exist`)  } })
      
      





▍ 404 error handling



If the request passed through all the intermediate handlers and routes, but was not processed, this means that the endpoint corresponding to such a request was not found.



To handle 404 Not Found



errors, you need to add, between the routes and the error handler, an additional handler. Here's what the creation of the 404 error object looks like:



 //  ... // ... app.use((req, res, next) => {  next(createError(404)) }) //  ...
      
      











Error Details



▍ ERR_HTTP_HEADERS_SENT error notes



Do not panic if you see the error message ERR_HTTP_HEADERS_SENT: Cannot set headers after they are sent to the client



. It arises due to the fact that the method that sets the response headers is repeatedly called in the same handler. Here are the methods that call to automatically set the response headers:



  1. res.send
  2. res.json
  3. res.render
  4. res.sendFile
  5. res.sendStatus
  6. res.end
  7. res.redirect


So, for example, if you call the res.render



and res.json



in the same response handler, you will get the error ERR_HTTP_HEADERS_SENT



:



 app.get('/testing', (req, res) => {  res.render('new-page')  res.json({ message: '¯\_(ツ)_/¯' }) })
      
      





As a result, if you encounter this error, carefully check the response handler code and make sure that there are no situations in which several of the methods described above are called.



▍ Error Handling and Data Streaming



If something goes wrong while streaming the response to the frontend, then you may encounter the same ERR_HTTP_HEADERS_SENT



error.



In this case, error handling must be passed to standard handlers. Such a handler will send an error and automatically close the connection.



 app.use((error, req, res, next) => {  //       ,        if (res.headersSent) {    return next(error)  }  //     })
      
      





Summary



Today I told you everything I know about error handling in Express. Hope this helps you write more reliable Express applications.



Dear readers! How do you handle errors in your Node.js projects?








All Articles