Skip to main content

Command Palette

Search for a command to run...

API Error Messages

Published
6 min read

When designing an API, we tend to only focus on successful requests. This often leads to error responses that are not useful and often don't contain any information. I am guilty of this myself, and in my own projects I typically only use the HTTP status code and do not include a body with more information. In this blog post, I want to explore what a good API error message should look like and which considerations we have to take into account.

First of all, I am talking about traditional REST APIs, and thus the usage (and correct usage) of the HTTP status codes is important. While we can use these to indicate coarse errors, they are often not sufficient. The type of errors we will be discussing in this blog post is the general 400 error. This error means that the client submitted a bad request, and it can correct it himself, if he only knows what the actual problem is.

Why do I focus on this error? Because a server error is out of the control of the client and returning more information may be useful for sysadmins, but a generic client can not do anything about this. Instead of returning information to the client, hoping he will report the issue back, you should just log this issue directly in your application logs. A 401 and 403 a clear by itself and a client should be able to handle these errors solely based on the status code.

The problem with adding more information to the error response is how you communicate the error. Do you use your own error codes? Do you include a message? I think the latter is more mainstream, and I have seen error responses like the one shown below. My biggest issue with this type of responses is, that while they are useful for a person, a client is still rather limited in the ways it can handle them.

{
  "errors": "The street field is required."
}

What can the UI do? It can show the error message to the user, but the user may not know which field it being talked about if the fields in the UI don't map on a 1-to-1 way to the fields in the API. A simple improvement would be to split off the errors into a list, and for each field explicitly mention the field. This allows the UI to show the error at the field(s) that are related to this error. The client should be able to do this, since it mapped the UI fields to the API fields in the first place. This type of reporting errors, does make it harder to map errors that cross multiple fields, as is the case with a start and end date. The start date should be before the end date, or is it the end date that should be after the start date?

[{
  "field": "street",
  "description": "This field is required."
}]

Both types still have a fatal flaw though, they include an error message, trying to be helpful. But this completely ignores the language of the user. Since it is very likely that the message itself will not be in the same language as the UI, the UI can't show the message as-is without breaking consistency. So should the backend translate the message for us? Meaning the UI has to submit a language with each request (or as part of the token, session, ...). I am not a fan of translating things in the backend for the UI. It is the UI who is responsible for this, but translating an entire message is not very easy and this also means that it becomes a hard part of the UI and can not be changed anymore without breaking the UI.

I have worked at a company where we had some software running on an embedded device, the approach there to show errors was by using error codes. Since the screen was very small, it was not easy to show an actual description for the error (but it was present in a small font on the top of the screen). Instead we opted to show the error code clearly on the screen, even the background of the screen would be turned red. All of this to make it easy to see that the machine went into error and allow for easy remote resetting (if possible). The error codes were organised in their own ranges like HTTP status codes. This has always stuck with me, and is in my opinion the best way to represent errors as they do not depend on a single language. The error code itself and the mapping is the actual contact. The client can map the error code to whatever message it wants.

There is however one caveat with the usage of errors codes, if you are not careful, you may end up with an every growing list of errors codes. It is not my intention to only report a list of error codes where each single error code corresponds to a single error for a specific field (like 1001 being the street field that is missing, 1002 being the zip field that is missing, etc...) Instead, I would opt for simply replacing (or keeping it for human readability) the description field with an actual error code. This means that your error codes indicate a type of error, and the field tells you which field it is corresponding too.

[{
  "field": "street",
  "code": 1000,
  "description": "This field is required."
}]

I would also advice to group your error codes into specific ranges. In the example above, the 10 range could be used for single field errors. A 11 range could be used for errors spanning multiple fields, or conditional fields. A 20 range could be used for something that is not directly related to the fields but some other sanity logic that field, such as an amount of requested items that exceeds the amount in stock.

With the specific ranges or types of errors, if you want to pass extra information, you may even opt to have different structures for different errors, but this feels a bit overkill. For example, in the case of the request amount exceeding the amount in stock, you could return the currently available items in stock in a separate field. This is however not needed, as the client can also make a request to the backend to get the amount left in stock.

I fully understand that you may feel uneasy to use error codes, as they are not as intuitive as messages, but after a while you actually get used to it and even start to know them out of the top of your head. The thing is that the error codes are not designed or meant for humans to handle them, they are meant for other applications.