Development / DevOps / Tools / Sponsored / Contributed

Error Handling from Backends to the Frontend

1 Nov 2021 9:00am, by

In the 1990s, my Mac OS 7.5.1 would generate an error like this:

Mac OS 7.5.1 error

Why did I have to reboot? And so often! The errors were ambiguous and modern exception handling didn’t exist yet. In Windows 98, you’d get the same ambiguous “blue screen of death.” The reason was because Windows/Macintosh used a shared memory model before protected memory existed, so it was easy for an application to overwrite system memory and crash the kernel. Operating systems have evolved since then, and we’re all happier for it.

Windows blue screen

Errors were quite frequent and annoying in the ’90s, always requiring a reboot. When a memory error happened, you’d get ID = -1 or a hex memory address in Windows corresponding to the cause. These were called error codes, but they weren’t exceptionally helpful or user friendly unless you were an expert developer.

The Old Way of Handling Errors

Jonathan Kelley
Jonathan has been a technologist for 14 years, with a focus on DevOps for half of that. He’s currently a site reliability engineer at LogDNA, where he contributes his expertise about Linux, Kubernetes, networking and cloud infrastructure.

Imagine if you had a C function that took as arguments an array/list of integers and a single integer, and then performed a search. It should return the index of the first match in the index. But what if it didn’t? You’d return -1 or something. That makes error handling simple, right?

The problem with this approach is that every caller has to check the return function and handle the error on its own. Lots of checks and “if else” statements in your code will make tons of boilerplate that distract you from the actual business logic. The code was terrible to read and maintain.

All too often a return ID was -1, which meant “something broke, but I don’t know what.” What do you do then?

Modern Exceptions (Try) to Save the Day

Exceptions exist to solve all the problems mentioned above. An exception can interrupt software control flow and bubble up to the user with informative data. This is great, but to most users it won’t mean much of anything.

When designing your application, always try to catch exceptions you think you’d encounter and then have a generic catchall for cases where unknown problems can be caught. Perform a logging action that’s user-informative or has some other value, so the end user can troubleshoot the cause.

To catch an exception in Python using the HTTP requests library, you could do something like:

This is what you’d see when you visit a non-existent URL at Google:

Http Error: 404 Client Error: Not Found for url:http://www.google.com/bogus

See? Makes a lot more sense than Python’s uncaught exception handler (below):

You want to handle all exceptions, even if it’s just to display a friendly error message and exit the program anyway.

Good exception handling should also be used to free up resources, file resources, locks or other things that could break future execution. A super-simple example (don’t do this in practice in Python, you should use Context Manager instead) is to perform something like:

Always Use the Most Specific Exception

The more explicit the exception, the better you’re handling errors in code. If your code is using generic try / except blocks everywhere without a specific exception to trap, you have no control over how a specific exception is handled. This also makes exceptions unknown to the caller, which is terrible if a user or developer doesn’t understand the failure modes for your library. You should also investigate the libraries you’re using and understand the exceptions they might throw, so you can take action.

Likewise, if you’re developing an application, raise specific exceptions. Raising a generic exception makes your code brittle for error handling by downstream developers. You want your calling functions/methods to be able to handle exceptions and hopefully without their own specific checks around your library’s function.

Never Log and Re-Throw an Exception

Here’s an example where you carelessly casted a long integer as a string and perform the “catch and throw” anti-pattern. I’ve seen this a lot in Java code for some reason, so here it is:

The problem is you get multiple errors for the same exception; it’s just ambiguous and unnecessary. The extra stack trace doesn’t add any new information, other then to expose the entire Java stack that called it.

If you want to add a stack trace with the error, throw a custom exception that has useful information for your logging library:

Handling Frontend Exceptions

A lot of frontend applications are designed with an optimistic approach of assuming a backend always works, but that’s never a 100% guarantee. Even bug-free backends run on systems infrastructure that will eventually fail. I’ve been a systems engineer and site reliability engineer and seen so many failures in my career, that I expect all systems to fail, at least partially, sometime in their production life.

Handling errors on the frontend is critical to user experience and functionality. How can a support staff understand and support a product when it breaks? How can QA make bug tickets if you don’t have anything to show when errors happen?

Types of Frontend Errors

Here are the usual errors you’ll see:

  • Backend availability errors: The system that a frontend consumes goes down for some reason. Likely a server crash, deployment or unexpected maintenance.
  • Authentication/authorization errors: This happens when a user doesn’t have permission.
  • Input errors: This happens if validation is missing in the frontend but gets caught or thrown by the backend. Could be input validation or unique constraint errors in a database, for example.
  • Unknown errors: Sometimes errors just happen. API 500 errors due to an unhandled code exception can always happen, because sometimes you just forget to predict where backend code fails.

How Frontends Break With Unhandled Backend Errors

Frontend apps usually have little in the way of handling the backend errors. I’ve seen the following modes of failure in JS webapps:

  • If the error happens during framework initialization, users may see a white page.
  • The framework stops working with no indication of why. The user tries an action again, but either the web page is locked up or nothing happens. The user refreshes the web page and hopefully gets a working application.
  • The framework keeps running but unexpected things happen. The user likely tries over and over, hopefully getting the response they want, but possibly causing unintended side effects on the backend. A terrible example would be hitting a payment gateway and getting double or triple charged!

Put Errors in Context For the User

You want to start by designing your backend to handle errors as gracefully as possible, to give something for the frontend to present to a user. When designing a REST API for instance, instead of just returning 500, check out the list of available HTTP codes in the Mozilla developer center.

I also suggest returning a body instead of an empty document with error generation so the web application can “bubble up” messages to a user. Something like this is excellent:

An error like this is great, because your application can tell the user something useful. So if they contact you for support, you’ll know exactly where your app went wrong. This is way better than the classic “Oops! An error occurred!”

Frontend Error Handling is Vital

It’s best to tell a user what went wrong and hint at what will fix the error for them. Here are some keywords I’d use to let a user make sense of frontend errors.

  • Invalid input. If the backend had an input validation error, tell the user.
  • Try again later. Something broke, but maybe it’ll work later. This lets the user know that this problem isn’t their fault and might resolve.
  • Unknown error. Please contact support. Something broke badly, so maybe you should contact a support team so they can determine the next steps.

You should write your frontend to use json stringify to parse the backend errors and return with valid context and messaging for the user.

Conclusion

Just keep in mind that errors always happen, perfect test coverage or not. The question is what will you do to handle them with excellence? Will your users be able to take action to save the user experience when errors occur? Will they have a good user experience even when the app fails?

Make it easier for users to create helpful GitHub issues or send useful messages to a support team. When your junior developer reads a bug ticket, will it be clear what happened without having to spelunk through the entire codebase?

Handling errors appropriately keeps our users happy.

Photo by Yan Krukov from Pexels.