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!
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.
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.
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
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.
- 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.
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.
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?”.
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’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.
If you want more information on logging, and are curious about a methodology for logging, I highly recommend the article: You’re Logging Wrong: What One-Per-Service (Phat Event) Logs Are and Why You Need Them.
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.
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!
Lou is the editor of The Cloud Native Software Engineering Newsletter a Newsletter dedicated to making Cloud Software Engineering more accessible and easy to understand. Every month you’ll get a digest of the best content for Cloud Native Software Engineers right in your inbox.