Software engineers write programs that work. Great software engineers write programs that work in almost any condition.
You just spent 5 minutes typing in the name, addresses, and lots of other things in an online government form and got the “Server connection closed” message after clicking on “next.” You check in to a flight using a self-check-in terminal, and after finally solving the quest of using its interface, you got “Sorry, an error happened.” You are making a money transfer using a mobile app and step into an elevator where there are no signal and app freezes.
Not good enough error handling in software could cause a mild annoyance, frustration, and could even create problems that would take hours to resolve. And not only that. Bad error handling makes the product look unfinished, raw, sloppy. It undermines users’ trust.
So it’s good to have some thought put into “what could go wrong” scenarios and have them handled. It’s good to have reliable software that just works and doesn’t require the whole team of support engineers to put up fires over and over again. And part of this effort is to keep the code clean and have a consistent approach to error handling.
We’ve already discussed the difficulties with using the most common vehicle to deal with errors — exceptions, and also looked at what good does it give. Based on the observations, let’s try to come up with a few ideas on how to make our software reliable.
Idea #0. Guidelines (aka code style)
There is a lot of complexity in software engineering: even a simple webpage that displays some information from a database involves technology developed by thousands of people over the course of a few decades. And the complexity could be essential — because our problem is complex, or accidental — because the solution is convoluted. In other words, essential complexity is one that allows solving the problem, and accidental is one that just makes your life harder.
A natural way to get some of the accidental complexity is not to have code style. Then different projects across the organization or even different parts of the same projects would have different styles: variables named differently, different indentations, etc… It’s bad for two reasons:
- Each time you are looking at code, you have to spend a little bit of time figuring out what is what. If code style is uniform and you see
MAX_DELAYin Java or
kMaxDelayin C++ you know it’s some constant
- Each time you encounter some rule, you personally don’t like you might spend some time “fixing” it.
That’s why pretty much every company develops or uses style guides. The main value from it is not that it chooses “the best” style but that it chooses it. Uber’s “standard code style” puts it nicely:
The the whole point of
standardis to avoid bikeshedding about style. There are lots of debates online about tabs vs. spaces, etc. that will never be resolved. These debates just distract from getting stuff done.
So it could be useful to develop guidelines for handling errors and stick to it. Though guidelines themselves are just the beginning.
It should be easy to do the right thing: libraries for relevant languages with all utility classes should be provided, as well as dev tools to take care of trivial things like indentations and variable naming.
And it should be difficult to do the wrong thing: there should be both technical (presubmit checks that won’t allow submission if there are style errors) and organizational (code review) mechanisms to enforce code style.
We could use the same approach for error handling. Have a few guidelines when dealing with error scenarios, so error handling becomes a matter of following these rules.
Idea #1. Use both exceptions and statuses
Sooner or later, there would be a case where we want to return either something or an error, which isn’t something exceptional.
The simplest example is to pass JSON provided by the user. Honestly, we don’t treat “user-provided JSON is invalid” as something exceptional — it’s a normal flow of things. One could argue that we could have two methods instead: “validate” and “process.” That’s a fair point, and it’s probably correct from an idealistic point of view. But in practice that would entail requiring the user to call two methods instead of one and making a data class for that JSON — ok things to do, but extra work. And we would still have cases where we want to return either value or error.
Some languages have native support for this pattern (e.g. Scala’s Try or Go’s multiple return values), for some language a custom should be created (though it’s shouldn’t bee too difficult): e.g. Google’s
Idea #2. Use exceptions for exceptional cases only
If we’re using both of the tools, we need a way to distinguish cases where to use statuses and where to use exceptions. You can start with the rule “throw an exception if and only if nothing could be done and current work should be aborted entirely” and see if it works for you.
Idea #3. Have a set of standard errors and use it everywhere
In the previous article, we discussed that an engineer should answer a few questions to handle the error:
What if our guideline is the following: “always use standard errors?” That’s two fewer questions to think about (3 for Java). And not only that.
With custom errors, there would be additional problems:
- Discover already present exception/error status. Would custom exception classes be located in
exceptions? Or maybe
errors? Is it in our project or in some sort of
commonshared library? Which
common, or maybe we have
common-errors? I’m sure you had this situation.
- Deal with similar or even completely equivalent statuses or exceptions. In this module we use
mapper.errors.NotFoundError, but this library returns
geo.statuses.NotFoundErrorStatus. So we should probably check and convert the latter to the former.
These problems are eliminated entirely if a set of standard error statuses/exceptions is used. E.g. use one from Google. Is the file absent? Return
NOT_FOUND. Invalid data was provided —
INVALID_ARGUMENT. Request to withdraw submitted before money arrived —
FAILED_PRECONDITION. Sometimes the choice might be tricky, but these 15 codes should work for 90% of the cases.
For the remaining 10% of cases, we would have to create custom error codes/exceptions.
Idea #4. Treat custom error same as a custom class loader
It’s very easy to create an exception. Create a new class, extended it from
std::exception and done. Does it mean custom exceptions should be all over the place? It doesn’t.
Exceptions or error codes should be treated similarly to custom class loaders or memory allocators. Or custom collection type. You won’t create
CustomerArrayList so don’t create
CustomerNotFoundException. Errors are part of the infrastructure, not business logic.
All these measures are supposed to help with accidental complexity, help to focus on important things, and not on solving riddles of implementation details.
Though probably not all of these ideas will work for you as is — no silver bullet. There is no single technical answer to get error handling right.
Getting error handling right is partially about technical details: what works best for your project and codebase, but a big part of it is about communication and leadership: how to get other engineers on board, come up with good enough approach, and do it in a reasonable amount of time. The ideas here are not ideal, but it could be a good starting point to work out a different set of guidelines that would fit your team and organization.