A Simple Approach To Fullstack Error Handling

April 11, 2021 · 4 min read

Generic error messages suck.

As a user, we encounter them all the time. “An error occurred, please try again later“. Often just to find out the problem wasn't some server outage, but something on our side. A typo in our email - and the server couldn't send that verification email, resulting in a 500 internal server error.

Yes, there are myriads of edge-cases if you develop anything for the internet. But there's an optimum somewhere between "if something's wrong we'll just send a 500" and over-analyzing every edge-case.

Here's a simple but effective way to API error design.

Short Recap Of HTTP Status Codes

Wikipedia lists 60+ official and 40+ unofficial HTTP status codes.

How do you know when to use which?

I'll just focus on what's important for developing web-facing REST APIs. Other API types may need more or other status codes.

There are four relevant categories of status codes:

  • 2xx success - request was handled successfully and did what was expected
  • 3xx redirect - you may find what you're looking for here …
  • 4xx client error – this particular request couldn't be handled because the client did something wrong
  • 5xx server error - something unexpected happened on the server side

For API design, you'll likely not use 3xx status codes. And if you are, I'm sure you know what you're doing.

Here are the 6 HTTP status codes you'll use most often:

  • 200 OK - everything worked as expected. Your response body can contain any requested data. Other 2xx status codes you may be using are 201 to indicate a resource has been created, or 204 when you intentionally return no content. But most clients will gladly accept any 2xx status code, so you can also just stick to 200.
  • 500 Internal Server Error - an unexpected server error occurred. This can be, e.g., a failing DB connection, an unavailable upstream service, or an unhandled exception. There are other 5xx codes such as 502 Bad Gateway, but web frontends usually don't care what exactly the error was. 500 is fine.
  • 404 Not found - the requested object or route doesn't exist.
  • 401 Unauthorized - the user isn't logged in or has an expired token (should be called Unauthenticated IMO).
  • 403 Forbidden - the user is logged in but doesn't have the necessary rights to perform this action.
  • 400 Bad Request - the user is logged in and has the necessary rights, but the request failed for a reason innate to the request.

5 Best Practices for Good-Enough API Error Design And Fullstack Error Handling

Validate forms in your frontend. It's so much nicer to the user and your backend. The user gets helpful error messages for their inputs immediately, and the backend doesn't get unnecessary requests. Check e.g. that phone numbers contain numbers, and email addresses look like email addresses.

Validate all incoming requests in your backend. Validation in this context means to check if the request has the right shape. Does it contain all necessary properties? Are string lengths ok? Is that email formatted like an email? Check everything you can check synchronously. The format of this response doesn't really matter, as long as its descriptive and explains the error. Validation on backend-side should never be relevant in production as the frontend also validates stuff. It's just relevant to protect the backend and as a support for the frontend developers integrating against the API.

Use 401, 403, and 404 appropriately. Always tell the user if they're accessing a non-existing resource, or if there are unauthorized or unauthenticated.

Use 400 with custom error codes in the response to foreseeable request errors. These errors will occur while processing a request. E.g. if your API looks up geo coordinates for an address, you can return a 400 Bad Request error with a JSON body of { "error": "Bad Request", "statusCode": 400, "message": [{ "code": "GEOCOORDINATES_NOT_FOUND", "description": "we couldn't find geo coordinates for the given address" }]. Using an array makes sure you can return all relevant input errors for one request.

Everything else is unforeseeable. For that just make sure your backend logs such things properly so you can fix them. Also make sure the backend doesn't return the stack trace, just a generic 500 internal server error, and that the frontend informs the user of a technical error.

This approach improves backend security, frontend developer experience, and most important the user experience for your service.

How to start

Take 20 minutes and go through the most important input errors a user can make that you can't detect in your frontend.

Then make sure you are returning a proper (custom) error code for this case.

Then add frontend code to handle these (custom) error codes and to inform the user.