Error handling can be a confusing topic — for a long time I struggled to understand error handling myself. I found the whole topic quite mystical and daunting. I ended up subscribed to the school of thought: “let the error throw and pray”. But, over time, I learned there are simple, easy to understand strategies for error handling that lead to noticeably better results than hope alone!
If, like I did, you also have difficulty understanding error handling then you’re in the right place. After years working with JavaScript, and speaking/working with other engineers, a style of applications layout for handling errors emerged in my own work. And it’s this philosophy for error handling that I want to share with you today.
By the end of the article you’ll understand how to structure an application to handle errors effectively, achieve more understanding of the application, deliver better error messages and have an easier time debugging.
Anyone who has been to the website before knows that I mainly write about Cloud Engineering. Which begs the question: What does structuring JavaScript applications have to do with Cloud Engineering? And why should anyone care?
Let me answer that: A well thought out application (with regards to its error handling) can help with monitoring, debugging, and ultimately providing a better experience to users, through more detailed bug reports. Handling errors effectively makes working with and maintaining a service considerably easier.
Error handling is also a key component of a good monitoring strategy, an application that handles errors well is easier to monitor, and a good monitoring strategy is a critical part of writing high quality cloud software.
But I imagine by now you’re itching to see some code, right?
Service Structure: A Complete Example
Let’s begin by looking at a complete example of an application structure with effective error handling. And also, don’t worry if it seems a little overwhelming at first, as we’ll break down the different parts as we go.
This is the pattern I use every time I write a new JavaScript service. The pattern works in any type of application whether that’s back-end services, serverless functions, container-based REST applications, or even front-end applications.
Before we break down the example code to examine the reasoning behind the pattern, let’s go top-to-bottom through the example and discuss each part.
To start, we have two sets of errors: A CustomError
, and a potential series of additional errors which extend the CustomError
base class (why we do this is explained later). In our case, to keep things simple, we only have one defined custom error so far, named InputError
.
Then we have a wrapper
function. This wrapper function should be used to encapsulate all logic in our application, therefore ensuring that all functions are executed in the context of the try/catch
. Caught errors are inspected using instanceof
to see whether they are an instance of our explicit custom error, or if they’re an unknown poorly handled error (not good, more on this soon).
Finally we have a function called businessLogic
. This function acts as a placeholder for where the business logic of our application would be. In simpler terms, it’s where the stuff that our application “does” would live. In this case we’re parsing JSON that’s invalid, and an error is expected to be thrown.
That pretty much covers the “what” of the code example, but we didn’t really the cover the “why”. why do we structure applications this way? What advantages does this pattern give us? The first step to understanding the “why” of this error handling pattern is to first understand some principles.
Error Handling Principles
To help us understand the example, let’s take a step back. When it comes to error handing in JavaScript, I stick to these main principles, and they can help us explain why structuring our applications in the above way is beneficial.
- Throw errors explicitly — Everywhere a possible error could be thrown, a custom error is constructed and given unique info.
- Catch & record all errors — All code is executed inside a try/catch where any unhandled errors can be caught and handled manually.
- Add context to errors — To aid the quality of our errors, and debugging we should seek to add context to all our errors.
Okay, now that we’ve got our principles, let’s turn our attention back to the original example and look at how these principles work in real life.
Principle 1: Throw Errors Explicitly
Caption: Image from Unsplash
The phrase “throw an error” in this context means: To wrap code in a try/catch
and throw a custom error object with sufficient information and context for the purposes of later debugging or to give information to the application user.
But why is throwing errors explicitly such a good thing?
- For applying unique error codes — Each caught error can be assigned an error code which is then used by the user to understand what the error means and potentially how to recover or fix the issue. We also use this unique code to identify re-occurring errors in our application.
- For differentiating known and unknown errors — By handling all errors our attention is drawn unexpected errors—errors we didn’t explicitly handle. These errors are interesting because they likely occur in scenarios we didn’t anticipate and warrant investigation.
- We can choose our error “zone” — An error zone is the “width” of our code in which we want to handle a given error. A wide zone gives a less conclusive error. A narrow zone is more conclusive, but costs more effort in adding error handling in our code.
When we are handling all errors we can start to understand more about our applications, and we can extract more information from our errors both on an individual occurence level, and on an aggregate system-wide behaviour level.
In summary: All code that could throw an error should be wrapped in a try/catch with an explicit, detailed error being thrown.
Principle 2: Catch & Record All Errors
Caption: Image from Unsplash
To compliment principle 1, of explicitly handling all errors, we should catch and record all of our errors. But again we have the same question: why should we?
When we allow errors to “just throw” without catching them, we lose the opportunity to log our error and leave additional context about why the error might have occurred, which is useful for debugging.
When we handle errors, rather than receiving some cryptic syntax error, we’d ideally receive a well written, plain language message alongside a code which would identify that unique occurence of our error (more on this later).
Now at this point you might now be wondering: “But how do we catch all errors? What does catching and recording errors look like in practice?”.
Some frameworks / tools often give us an event to “hook” into any unhandled errors. However, if you’re just using vanilla JavaScript you can write a wrapper
function as we did in our original example to catch all of your application errors.
Once you’ve caught your errors, you’ll likely want to do something with the errors. The minimum is usually to log the error either for the application user, or for later analysis. Logs are generally formatted according to your tooling.
If you work on a back-end service, you’ll likely be logging to the “stdout” of your application, in JavaScript that’s as simple as a console.log
statement.
If you’re in the front-end world, you’ll probably want to send logs to a logging tool via HTTP. Many front-end tools exist, such as: sentry and bugsnag. Or, you may want to create your own service / API for tracking errors.
In summary: All errors in an application should be caught and dealt with, not left to throw and crash our applications.
Principle 3: Add Context To Errors
And the last principle we’ll discuss today is about how we add context to errors. We’ve talked about the fact that we should always handle errors, and we should always catch them and do something with them. But we’ve not yet discussed how to decorate errors to give them appropriate context.
You should recall in our original example we defined a CustomError
class. And it might have left you wondering “Why”? There are indeed many other patterns we could have used, so why use a class for our error handling?
The short answer is: Convention.
But the longer answer is… since we’re discussing error handling and adding context to errors, we want to use a pattern which allows us to add context to an error, and an error object is perfect for the job.
Let’s extend our original example somewhat to show you what I mean…
In this example we’re now taking our original example further, rather than just checking the type of our error, we’re also now extracting properties from the error to log to our user. And this is where things start to get really interesting!
As you can see, we are now attaching additional information to our errors, such as an instance error code. Instance error codes help us to identify unique occurrences of a given error within an applicaton.
When we see an error code within our logs we now know exactly which part of our application threw the error. Knowing where in our application helps us to not only debug, but identify hot spots and correlation in errors.
For example, you may have a question such as: “Are all users in a given country getting the same error?”. Using error instance codes you can find the answer.
Hopefully you can start to see how, through adding error context, we can start to gain better insights into how our applications work.
In summary: Add context to errors when they’re thrown, such as instance error codes to make it quicker for tracking down and fixing errors, bugs and improving the debugging experience of your application.
Don’t Hope And Pray: Handle Your Errors
And that concludes our philosophy for error handling in JavaScript.
To quickly recap, the philosophy is based on three principles: Firstly: Throw errors explicitly. Secondly: Ensure you catch thrown errors. And finally: Add context to your errors where possible (using custom errors).
Now you hopefully have a good starting point for tackling errors within your application. And I hope that you won’t do what I did, and spend your time writing code where errors simply throw all over the place!
Because when you do only throw errors, you throw away the insights which you could use to debug and improve your application, improve user experience, and hopefully make your life easier.
Speak soon Cloud Native friend!
What are your principles for error handling?
- 2023 Summary: Data Driven Stories About The Cloud - December 31, 2023
- 2022 Summary: The Open Up The Cloud System - January 1, 2023
- Open Up The Cloud Newsletter #30 (January Recap 2022) - March 1, 2022