Error handling is a necessary and very important aspect to any API. Express’ error-handling middleware makes handling errors simple and clean and allows for the flexibility to create pretty advanced and useful error-handling tools! A typical simple handler might look something like this:
...
// This will always throw an error
app.get('*', (req, res, next) => {
throw new Error('An Error Occurred')
})
// This should catch that error and handle it
app.use( (err, req, res, next) => {
// Maybe some logging here...
res.status( 500 ).send({ message: err.message })
})
...
This is great! Any errors should bubble up to this handler and return to the user a nice error message with a 500 status code. But what if you want more granular control of how your errors are handled within a central error handler? Let’s look at one way to organize our error handling logic to allow for providing custom error handling and logging that can vary for specific types of errors.
Organizing the Handler
There are countless ways we can go about organizing the handler’s logic. In this example I will separate the handler’s concerns into a couple different files:
/app
index.js
/util
/errorHandler
errors.js
index.js
reducer.js
index.js
The base file for our API which prepares and starts up the application/util
This folder holds the utility modules (in this example, we only have error handling)/util/errorHandler
The folder containing the files for our error handler middleware.index.js
The main file for our error handler. It provides the middleware function that will catch errors and respond to the defined actions that need to take place for each kind of error.reducer.js
This provides areducer
, a function that defines how to respond to something based on an input. We are going to use this to decide how to respond to the API’s user when certain errors are produced.errors.js
This file is just a JSON mapping to define custom errors we will catch. These are similar to Actions in Redux.
Basic Error Handling
Now that we have an idea of how we’re going to organize the application and error handler, let’s get the error handler set up so we can build upon it! To do this, we’ll start by creating the actual error-handler middleware function in /utils/errorHandler/index.js
that we will provide and use in the base of the application so it can catch any errors that bubble up.
// Handler for errors
module.exports = (err, req, res, next) => {
// Send the error response
res.status( 500 ).json({ error: err.message })
}
That’s about as basic as it gets! This will catch any error and return the error message with a 500 error status. Now we can tell express to use this middleware to catch errors. Here is the main index.js
that starts up our application.
const
express = require('express'),
app = express(),
errorHandler = require('./utils/errorHandler')
// Sets up some routes to play with
app
.get('*', (req, res, next) => {
throw new Error(`Oof, I broke...`)
})
.post('*', (req, res, next) => {
res.json({ message: 'No breakage here!'})
})
// NOTE: We apply our errorHandler middleware LAST so that it can catch any bubbling errors
app.use(errorHandler)
// Start up the app
app.listen( 3000, () => console.log(`Let's catch these errors...`))
Note that we tell the app to use the errorHandler
middleware AFTER all of the other middlewares and routes. This is so that any errors that happen within those functions above it will get caught by the handler.
Let’s give our two endpoints a test and see what we get.
Here we GET the root of the API which generates an error. Notice the 500 error status!
Here we POST to the root of the API which completes successfully. Notice the 200 success status!
This is the most basic example of error handling using Express’ error-handler middleware. With this setup we will always generate a 500 status and return the error message. No logging, no other processing. It does the trick, but we can do better! Let’s spice things up a bit.
Customized Error Handling
Now that we can catch and handle errors, we can start thinking about what else we can do with our error handler such as handling Operational Errors (errors that are not caused by bugs in the code), logging errors, etc…
To get to that level of error handling, we can start by putting some logic in place that will allow us to send different error codes and responses based on which error is being thrown. There are some scenarios where we may want to send a status other than 500 or a message other than the thrown error message, and we will look at one of those scenarios below. To do that, I’ve chosen to create a sort of reducer (similar to a reducer in Redux). It will take in the error and, depending on what the error is, decide which status and message to send and will tell the handler whether or not to log the error.
Let’s set up a custom error in our errors.js
file so we can define what the error handler’s reducer should look for to handle this error. These will be similar to actions in Redux.
module.exports = {
CORS_ORIGIN: 'cors/origin'
}
Sweet! Now we need to throw the CORS_ORIGIN
error when an unauthorized Origin attempts to hit the API. To do that we should throw an error with a value of cors/origin
.
// index.js
const cors = require('cors')
const whitelist = [
'http://authorized-origin.com'
]
const corsOptions = {
origin: (origin, callback) => {
if (whitelist.indexOf(origin) !== -1) {
callback(null, true)
} else {
callback(new Error(`cors/origin`))
}
}
}
...
app.get('/cors', cors(corsOptions), (req, res, next) => {
// This is the message that gets returned if the cors check passes!
res.json({ message: 'CORS check passed successfully!'})
})
Next let’s set up a reducer that will catch that error.
// reducer.js
const errors = require('./errors')
// Default Error Details
const defaultDetails = {
status: 500,
message: 'Something failed!',
logError: true
}
// Defines how to handle individual errors (typically will be used for special cases)
module.exports = err => {
switch ( err.message ) {
// Handle CORS errors
case errors.CORS_ORIGIN:
return {
...defaultDetails,
status: 400,
message: 'Not authorized by CORS',
logError: false
}
// Handle the default action
default:
return defaultDetails
}
}
In the code above we set up a default set of error details. If the error passed to this “reducer” function doesn’t match any of the defined error cases, it will just return the defaults. If it does match a case, it will return the details specific to that error.
We write it this way so that errors that are generated by problems with the code, or errors that are given a generated error message, will be handled as well as any custom errors that are defined by the developer. These could be things like user-input errors that we wish to inform the user of.
With that built, we can now import that reducer function into our error handler file and use it to determine what responses to send back to the user of the API.
// /util/errorHandler
const reducer = require('./reducer')
// Handler for errors
module.exports = (err, req, res, next) => {
// Get the error response details relevant to this error
let { status, message, logError } = reducer( err )
// Should I log this error?
if ( logError ) {
// You could add custom logging here.
// For simplicity, I am just console logging
console.log({
message: err.message,
stack: err.stack,
method: req.method,
path: req.path
})
}
// Send the error response
res.status( status ).json({ error: message })
}
Now we are able to define custom error details for specific errors! As you can see above on line 6, we pass the error into our reducer. The reducer decides which details to return based on the error that was thrown, and then we use that data to determine the status code and error message and to decide whether or not to “log” the error (currently we are just logging it to the console).
NOTE: We are using object destructuring to pull out the data we want from the reducer’s return value.
With this in place, we can now get a custom error response when CORS catches a bad Origin!
Here we request the API with the authorized origin so we get a success!
This request was from an unauthorized origin, so the
CORS_ORIGIN
error was thrown! Notice the 400 response and custom error message
Sweet! Now we can set up more custom errors and define how we would like to handle them! Using this structure, we could also build in a way to create analytics on specific types of errors, log to different locations based on the error, etc…
Conclusion
This is just one of the many ways you could organize and structure your error handling middleware. Using this as a base, your error handler can have the flexibility to serve many functions that aid in the debugging and logging of errors as well as accurately informing a user of the API of what happened to their failed request.
Thanks for the read, go catch some errors!