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:
- Sets the HTTP response status code to 500.
- Sends a text response to the entity executing the request.
- 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:
- error
- req
- res
- 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:
- Generate and send a suitable response status code.
- 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:
- 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). - Error
401 Unauthorized
. This response status code is applied if the user has entered incorrect credentials (such as username, email address or password). - Error
403 Forbidden
. Used when the user is not allowed access to the endpoint. - Error
404 Not Found
. It is used in cases where the endpoint cannot be detected. - 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:
- The response status code.
- The message associated with the error.
- 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:
- Set the response status code to
400 Bad Request
(after all, the user entered incorrect data). This will be our first parameter. - 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:
- res.send
- res.json
- res.render
- res.sendFile
- res.sendStatus
- res.end
- 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?