When you’re building an API with Express it can be difficult to know how to send consistent error responses. The framework doesn’t seem to provide any special features for this, so you’re left to figure it out on your own. At some point you’ll probably find yourself wondering if you’re doing it "the right way".
As I mentioned in my ‘5 best practices for building a modern API with Express‘ blog post:
It’s very tempting when building an API to invent your own format for error responses, but HTTP response status codes are a great starting point as they can communicate a specific error state.
If you invent your own format you’ll have to build a bunch of extra logic into your API, and you’ll probably want to make sure it’s thoroughly tested too. Nobody wants errors in their error response code, right?! On top of that, it will also require clients – e.g. front end JavaScript – to implement additional code for handling the special format of your API’s error responses.
Wouldn’t it be nice if there was a simpler way, a tried and tested standard way, of sending error responses? As luck would have it, there is! The HTTP standard defines status codes which you can use in the responses from your API to indicate whether the request was successful, or whether an error occurred.
Here’s an example HTTP error response with the 400 status code, which indicates a ‘Bad Request’ from the client:
< HTTP/1.1 400 Bad Request
< Content-Type: text/html; charset=utf-8
< Content-Length: 138
< Date: Wed, 28 Oct 2020 20:11:07 GMT
If you want to send an error response like this, you could use Express' res.status() method:
res.status(400).end();
Follow the yellow brick road HTTP standard
By default the HTTP status code sent in a response from an Express application is 200 (OK). This tells the client that the request was successful and that they can proceed to parse and extract whatever data they require from the response. To indicate an error when sending a response, you should use an HTTP status code from one of the two error ranges defined by the HTTP standard:
- Client error 4xx - The client has done something wrong.
- Server error 5xx - Something has gone wrong in your application.
The MDN web docs have a great reference with all of the HTTP response status codes that are defined by the HTTP standard, along with explanations of what they mean.
Once you've figured out what error status codes your API should send in different situations, you need a way of getting those status codes into an error - this is where the http-errors
library comes in.
How to create errors with the http-errors library
Setting it up
First of all you will need to install the http-errors
library:
npm install http-errors
And then you'll want to require()
it in your application (after you require express
is fine):
const createHttpError = require("http-errors");
The http-errors
library offers two different ways to create an error object.
Way #1: Specify a numeric HTTP status code
The first way is to specify a numeric HTTP status code e.g.
const error = createHttpError(400, "Invalid filter");
If you want, instead of passing an error message string, you can pass an existing error object to be extended e.g.
const error = new Error("Invalid filter");
const httpError = createHttpError(400, error);
Warning! Be careful whenever you pass an existing error as you could potentially expose sensitive details about your application if it gets sent through in an error response. As you'll see in the next section, the default error handler in Express has safeguards built in to help you avoid this, but it's important to be careful about what details you expose in your API responses.
If you want to specify extra headers to be added when the error is sent in a response, http-errors
allows you to do this by passing in a properties object e.g.
const error = createHttpError(400, "Invalid filter", {
headers: {
"X-Custom-Header": "Value"
}
});
Way #2: Use a named HTTP error constructor
The second way of creating an error object is to use one of the named HTTP error constructors which http-errors
provides e.g.
const error = new createHttpError.BadRequest("Invalid filter");
The difference with this second approach is that you can only pass in an error message string - it doesn't allow you to pass in an existing error object or a properties object. For situations where you don't need them, I think this second approach is easier to maintain. It means don't need to keep looking up HTTP status codes to find out what they mean every time you revisit the code.
What's in these error objects?
Here are the properties which will always exist on an error object created with http-errors
, along with example values:
{
message: "Invalid filter",
// This statusCode property is going to come in very handy!
statusCode: 400,
stack: `BadRequestError: Invalid filter
at /home/simonplend/dev/express-error-responses/app.js:33:17
at Layer.handle [as handle_request] (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/layer.js:95:5)
at next (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/route.js:137:13)`
}
Now let's take a look at what you can do with an error object once you've created it.
The default error handler in Express
Express provides a default error handler. This error handler is called when you call the next()
callback function from a middleware or route handler and you pass an error object to it e.g. next(error)
.
There are two important things to know about the behaviour of the default error handler in Express:
It will look for for a
statusCode
property on the error object (error.statusCode
) - that's right, just like the one which exists on errors created withhttp-error
. IfstatusCode
is within the 4xx or 5xx range, it will set that as the status code of the response, otherwise it will set the status code to 500 (Internal Server Error).In development it will send the full stack trace of the error that it receives (
error.stack
) in the response e.g.
BadRequestError: Invalid sort parameter, must be either: first_name, last_name
at /home/simonplend/dev/express-error-responses/app.js:17:17
at Layer.handle [as handle_request] (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/layer.js:95:5)
at next (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/route.js:137:13)
at Route.dispatch (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/route.js:112:3)
at Layer.handle [as handle_request] (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/layer.js:95:5)
at /home/simonplend/dev/express-error-responses/node_modules/express/lib/router/index.js:281:22
at Function.process_params (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/index.js:335:12)
at next (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/index.js:275:10)
at expressInit (/home/simonplend/dev/express-error-responses/node_modules/express/lib/middleware/init.js:40:5)
at Layer.handle [as handle_request] (/home/simonplend/dev/express-error-responses/node_modules/express/lib/router/layer.js:95:5)
In production i.e. when the environment variable NODE_ENV
is set to production
, it will omit the stack trace and only send the name which corresponds with the HTTP status code e.g. Bad Request
.
Displaying a stack trace in production is a bad thing to do: it creates a security risk as it reveals internal details about your application, which makes it more vulnerable to a potential attacker, as they will have details about how your application is structured and what libraries you are using.
Putting it all together
Ok, so we know about HTTP status codes, how to create JavaScript error objects which contain a status code, and how the default error handler in Express can use them. Let's put it all together!
const express = require("express");
/**
* We'll be using this library to help us create errors with
* HTTP status codes.
*/
const createHttpError = require("http-errors");
/**
* In a real application this would run a query against a
* database, but for this example it's always returning a
* rejected `Promise` with an error message.
*/
function getUserData() {
return Promise.reject(
"An error occurred while attempting to run the database query."
);
}
/**
* Express configuration and routes
*/
const PORT = 3000;
const app = express();
/**
* This example route will potentially respond with two
* different types of error:
*
* - 400 (Bad Request) - The client has done something wrong.
* - 500 (Internal Server Error) - Something has gone wrong in your application.
*/
app.get("/user", (request, response, next) => {
const validSort = ["first_name", "last_name"];
const sort = request.query.sort;
const sortIsValid = sort && validSort.includes(sort);
if (!sortIsValid) {
/**
* This error is created by specifying a numeric HTTP status code.
*
* 400 (Bad Request) - The client has done something wrong.
*/
const error = new createHttpError.BadRequest(
`Invalid sort parameter, must be either: ${validSort.join(", ")}`
);
/**
* Because we're passing an error object into the `next()` function,
* the default error handler in Express will kick in and take
* care of sending an error response for us.
*
* It's important that we return here so that none of the
* other code in this route handler function is run.
*/
return next(error);
}
getUserData()
.then(userData => response.json(userData))
.catch(error => {
/**
* This error is created by using a named HTTP error constructor.
*
* An existing error is being passsed in and extra headers are
* being specified that will be sent with the response.
*
* 500 (Internal Server Error) - Something has gone wrong in your application.
*/
const httpError = createHttpError(500, error, {
headers: {
"X-Custom-Header": "Value",
}
});
/**
* Once again, the default error handler in Express will kick
* in and take care of sending an error response for us when
* we pass our error object to `next()`.
*
* We don't technically need to return here, but it's
* good to get into the habit of doing this when calling
* `next()` so you don't end up with weird bugs where
* an error response has been sent but your handler function
* is continuing to run as if everything is ok.
*/
return next(httpError);
});
});
app.listen(PORT, () =>
console.log(`Example app listening at http://localhost:${PORT}`)
);
Note: The default error handler in Express sends an HTML response body. If you want to send a JSON response body, you will need to write your own error handler. I'll be covering this in a future blog post!
Top comments (0)