Every couple of months, I’m in a meeting where a couple of developers start arguing about which HTTP status codes to use in their RESTful API or decide not to use HTTP status codes at all and instead layer their own error-code system on top of HTTP.
In my experience, HTTP status codes are more than adequate for communicating from servers to clients. Furthermore, it’s preferable to stick with this standard because that’s what most client- and server-side HTTP libraries are used to dealing with.
When it comes to which status code to use, the truth is that most of the time it doesn’t matter, just so long as it falls within the correct range. In this post, I’m going to outline what the important ranges are and when you should use each one.
If you control both the client and server, these guidelines should do just fine. If you’re writing a more generic RESTful service where other people are writing the clients, you may have to be a bit more nuanced. Either way, this rule-of-thumb is a good starting point to work towards the simplest solution possible for your particular problem.
The Main Status Code Ranges
In short, there are three status code ranges you need to worry about when sending responses from a server to a client:
- 2xx-range codes (which I usually just pronounce as ‘two-hundred range’ codes)
- 4xx-range codes (‘four-hundred range’)
- 5xx-range codes (‘five-hundred range’)
There are also other ranges (for example, 3xx-range codes), but many client libraries will deal with them automatically. For example, if your client is running in a browser, a 301 status code (indicating a redirect) will automatically be handled by the browser, even for an XHR request.
So which of the three main ranges should you use for a particular situation? Well, here’s my rule-of-thumb:
- 2xx-range codes are for when the server has been able to successfully perform the operation that the client requested.
- 4xx-range codes are for when the server needs to communicate to the client that an error has occurred that is the client’s fault.
- 5xx-rangecodes are for when the server needs to communicate to the client that an error has occurred that is not the client’s fault.
Let’s drill into each of these scenarios in detail to get a feel for how they work in the real-world.
2xx-range codes are the server’s way of saying “this request succeeded”.
Many client HTTP libraries will consider a status code in this range as indicating a successful request, and will treat it accordingly. For example:
- If you’re using using Angular 1’s
$httpservice and it receives a response in the 2xx range, the promise that the service gives you when you initiated a request will resolve.
- If you’re using the popular Node.js
requestmodule, it will call your callback with the response object.
Even HTTP server libraries work with this convention by default. For example, by default, SpringMVC will put a HTTP status code of 200 in your response if you just return an object from a controller method.
4xx-range codes are the server’s way of saying “this request failed and it’s your fault”. Examples of when this can happen include:
- Part of the request is seriously malformed
- The data in the request didn’t pass business validation rules
- There’s nothing at the particular URL (this scenario is often represented by a 404 status code)
- It’s been too long since they logged in and their session expired (often represented by a 403)
Client HTTP libraries will consider a status code in this range to indicate a failed request. For example:
- A promise given to you by Angular’s
$httpservice will reject.
requestmodule will invoke your callback with the
Similarly, server-side libraries can be configured to automatically convert certain exceptions that have been thrown by a controller into a specific 4xx-range status code.
The important thing about a 4xx-range code is that the client might want to look for it and treat it in a special manner. This doesn’t mean that it has to, it just means that it has the option of doing so.
For example, a client might check whether a request failed with a 403 because it means the user’s session has expired and the client should take them back to the login screen (with a helpful error message, of course).
Another scenario where a 4xx-range code can be useful is a validation error. A client can look for a specific code and give feedback to the user that they entered some invalid input. That said, in real-life this scenario is actually less common than you might think. A client only really needs to look for such an error code when validation can only be done on the server-side. For most cases, validation is performed by the client before it sends data to the server. So whilst it’s important to also perform that validation on the server, if the server detects a validation error that should have been detected by the client in the first place, that’s the fault of the client programmer, not the end-user.
Similarly, I have never encountered a scenario where a 404 returned from a RESTful API was actually caused by a mistake on the part of the end-user. It’s always been caused by a bug in my client that caused it to make a call to a URL that, for one reason or another, doesn’t exist. (Note that I’m referring to regular usage here. If a user messes with the client—for example, by hacking it in the browser—they deserve whatever they get).
Irrespective of whether a client wants to check for a particular error code, it’s important to have a generic error handler in place on the client side that checks for any 4xx-range error and, if it hasn’t been explicitly handled already, pops up a generic message for the user saying that something bad has happened within the client. For debugging purposes, it should probably log the details of the error somewhere—for example, a browser console.
These codes indicate that something serious has happened within the server that was not the fault of the client.
This is usually because a server-side programmer has stuffed something up (for example, a NullPointerException, an IndexOutOfBoundsException, or any of the billion other things a programmer can get wrong), or because a downstream service (for example, a database) is unavailable. Either way, there is absolutely nothing that the client can do that guarantees that a subsequent call will be successful.
Client HTTP libraries will consider a status code in this range to indicate a failed request. At the risk of repeating myself, this means:
- A promise given to you by the
$httpservice will reject.
requestmodule will call your callback with the err argument set.
Similarly, most server-side HTTP libraries will automatically convert any uncaught exceptions that occur within the server into a 500 error, which is returned to the client.
I have never written a client that caught a 5xx-range error and did anything other than display a ‘Something bad happened on the server’ message to the user.
Like 4xx-range errors, it’s a good idea to have a global error handler in place within your client that looks for 5xx-range responses. In fact, it can be the same error handler as what you use for uncaught 4xx-range errors.
So … Which Status Code Should You Use Within a Range?
Like I said: usually it doesn’t matter, as long as you get the range right. That said, for those few cases where it is important, I offer a few of pieces of advice:
Don’t Do Double Duty
If you choose for a status code to mean something specific, you should make sure that it won’t accidentally be raised in different scenarios. For example, if you wanted to use a 400 to flag a specific business validation error you want to report to the user, what’s going to be used if the request is entirely malformed? If you use 400 for both and a client bug results in some malformed JSON being sent to the server, it would be confusing for everybody if the user was to get some message saying that there was something wrong with their input.
In the case that you have a very specific scenario that the client needs to detect so that it can do some special behaviour for the user, I recommend picking an entirely new status code that is not already taken (within the appropriate range, of course). There are plenty to choose from. However, this is a surprisingly rare scenario.
Don’t Be Afraid to Create a New Code (If Absolutely Necessary)
Bogged-down in an argument as to what status code to use to handle a critical scenario? Maybe you should just pick an unused code from the range and move on. Just make sure it’s not one that already has a generally agreed meaning, as that would be doing double-duty. This includes 418 I’m a teapot—I used that accidentally once and it was kind of embarrassing.
Also, it’s especially important that you document custom codes. I recommend Swagger for documenting any non-trivial RESTful API, even if you control both the client and server.
Don’t Create a New Code Just for Fun
Finally, don’t try and come up with new codes for scenarios that the client doesn’t actually need to care about. If your client is already doing validation checks that are also being done on the server, it would probably be sufficient to just use a 400 for communicating failures of the server-side validation. If such an error were to occur, it’d be enough for the client’s default error-handling to just kick in and tell the user that something bad happened internally without spewing the gory details at them. It would then be up to the client programmer to figure out that they had missed a client-side validation and let something invalid slip through to the server to pick up.
So … What Should You Put in the Response Body?
A lot of people also get confused about when you should put a body in a response and what the body should contain. Status code lists contain some guidelines on this matter, but generally, I think the short answer is that you put something in the body if the client is going to use it. For example:
- If you GET an entity from an endpoint and get a 2xx-range response, the body is likely to contain some representation of that entity. These days, it’s often HTML, JSON, or (sometimes) XML.
- If you POST a new entity to an endpoint and get a 2xx-range response, the body of the response usually, at the very least, includes the unique ID of the newly-created entity. Sometimes it also includes the rest of the entity, echoed back to the client, but this is really up to what the client needs.
- If you POST a new entity to an endpoint (or PUT an existing entity) with values that are invalid, then as well as including a 4xx-range status code, the response might include a body with some information about exactly what the validation errors are. However, it’s only worth putting this information in if the client can do something meaningful with it.
Conversely, if a client isn’t going to use something, it probably shouldn’t be in the response. For example, if a serious internal problem occurs on the server that results in a 5xx-range response, I’m not a fan of returning the gory details in the response body, as you are exposing the internals of your system unnecessarily to the outside world. Instead, I think it’s just better for a developer to go and look at a log file to see the details.
Let’s Wrap This Up
Arguing over specific HTTP status codes or inventing your own error code system is usually a waste of time. In my experience, building client/server systems with REST, the rules-of-thumb I have described here maximally leverage HTTP whilst also keep things as simple as possible. If you control both the client and the server, there’s usually not much need for anything else.
Thanks to my colleagues David Johnson and James Sinclair for reviewing this post.