Error Handling from Backends to the Frontend
Errors always happen, perfect test coverage or not. The question is what will you do to handle them with excellence?
Nov 1st, 2021 9:00am by
Photo by Yan Krukov from Pexels.
LogDNA sponsored this post.
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.
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.
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:
import requests
url = 'http://www.google.com/bogus'
try:
r = requests.get(url,timeout=3)
r.raise_for_status()
except requests.exceptions.HTTPError as e:
print (f'Http Error: {e}')
exit(1)
except requests.exceptions.ConnectionError as e:
print (f'Connect Error: {e}')
exit(1)
except requests.exceptions.Timeout as e:
print(f'Timeout Error: {e}')
exit(1)
except requests.exceptions.RequestException as e:
print(f'Unknown Error: {e}')
exit(1)
except Exception as e:
print(f'Unknown Exception: {e}')
exit(1)
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):
Traceback (most recent call last):
File "/tmp/fail.py", line 4, in <module>
r.raise_for_status()
File "/usr/local/lib/python3.9/site-packages/requests/models.py", line 943, in raise_for_status
raise HTTPError(http_error_msg, response=self)
requests.exceptions.HTTPError: 404 Client Error: Not Found for url: http://www.google.com/bogus
myfile = open('test.txt', 'w')
try:
myfile.write('the answer to life is: ')
myfile.write(42) # raises TypeError
except IOError as e:
print(f'Could not write to file: {e}')
finally:
myfile.close() # will be executed before TypeError is propagated
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:
try {
new Long("some_string");
} catch (NumberFormatException e) {
log.error(e);
throw e;
}
17:44:28,945 ERROR TestExceptionHandling:65 - java.lang.NumberFormatException: For input string: "some_string"
Exception in thread "main" java.lang.NumberFormatException: For input string: "some_string"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Long.parseLong(Long.java:589)
at java.lang.Long.(Long.java:965)
at com.stackify.example.TestExceptionHandling.logAndThrowException(TestExceptionHandling.java:63)
at com.stackify.example.TestExceptionHandling.main(TestExceptionHandling.java:58)
public void wrapException(String input) throws CrazyCustomException {
try {
// do something
} catch (NumberFormatException e) {
throw new CrazyCustomException("I think you did something bad.", e);
}
}
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:
{
"error": {
"code": 500,
"reason": "Payment gateway timeout, try again later"
}
}
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.
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.
YOUTUBE.COM/THENEWSTACK
Tech moves fast, don't miss an episode. Subscribe to our YouTube
channel to stream all our podcasts, interviews, demos, and more.