Handling Exceptions: The Easy Way

Dmitry Si
ProAndroidDev
Published in
5 min readJul 9, 2019

--

The simple strategy for dealing with exceptions.

Exception handling is still a debatable topic nearly 60 years after the invention. Thirty years later Java pioneered the checked exceptions, another clever but often misused concept which drew a lot of criticism and was abandoned by followers like Scala and Kotlin. Anyway, the exceptions are still there for most of the application-level languages, and handling is on us.

So how should we work with exceptions? I want to advocate for the simple strategy I follow for the last several years.

Pre-conditions

Before starting with the strategy itself, let’s cover the typical edge cases and the assumptions it relies on.

Not an exception — don’t throw

To handle the exceptions properly, we should only throw the proper exceptions first. Start with Java official documentation (same for C#), consider best practices, and never throw unneeded exceptions anymore. Exceptions meant to be exceptional. Negative and empty results are not exceptions!

Can’t control — accept

In some cases, we can’t control how an exception should be handled (e.g., checked exception in Java) so there is no need to think about it: we should follow the platform recommendations. When file-reading function always throws EOF exception we have no option but to handle it.

Don’t care — ignore

Finally, if we don’t care about some exception, we can catch and ignore it. There is a bunch of libraries that don’t follow the best practices and throw random exceptions here and there. If the exception doesn’t provide the information and doesn’t mean any unrecoverable state or observable error, there is no need to waste time on it. Don’t forget to document the reason for ignoring the exception, though!

The strategy

Now, when everything else is covered in pre-conditions, we can focus on the actual exceptional cases such as NPEs, various I/O problems, etc. These are the real issues that may happen during the app execution and cause the unwanted and observable result to the end-user. The strategy is just three simple questions, ask them in the same order to find a way of handling the particular exception.

That gives us four possible actions; let’s see how they work.

Avoid

The best exception is the exception that was never thrown. If the library throws when not initialized — initialize it. If the method can not work with nulls — make sure it impossible to pass the null. The code with lots of try/catch blocks is even harder to read than the code lots of if/else. Compare two approaches:

the difference is even more apparent when we only care about the positive scenario

Recover

Now we have a possible exception, say we need to dereference nullable object or perform an operation that may fail. Can this piece of code recover from that exception? I mean actually recover. For example, a networking layer can retry the call if it allowed doing so, but it can barely deal with the unparsable result, or the WiFi turned off. So it should not! No need to construct a fake result: yes, a banking app can display the account balance of $0.00 if it can not find the account but should it? An attempt to add the proper recovery code most likely will end up with the violation of SRP and an extreme number of injected dependencies.

Pass

Now we know that the piece of the code deals with an exception it unable to handle, but is that possible during normal execution? If yes and we already know, that we can neither avoid, nor recover, then the only action we can take is to wrap the exception to make it more specific and informative and re-throw. The upper layer, in turns, could be able to recover from that situation; in the example above it can refresh the list of accounts, or ask to reconnect WiFi.

Crash

Finally, if the answer to the last question was no. What does it mean that exception is impossible during normal execution? That means we violate some invariants that must be maintained by our system. An example would be the dereference of a nullable/late-init field that supposed to be injected in a class that’s always instantiated via DI framework. In other words, we own and fully control the code, and it depends not on the environment (user, network, disk), but on usage. We may get such an exception only if we add incorrect instructions inside the system. And if that happens, we’d better know about it as soon as possible. Crashing may be unpopular advice but let’s consider the alternatives:

  • Catch it and try to handle: that will result with the bloated code pretty much immediately. Can we put initialization checking logic into every single internal method? Shall we wrap every dereference of the variable that’s never null? Moreover, if the handling is complicated, it can cause even more bugs and will make the unit-testing harder for sure.
  • Swallow and ignore: that will lead to unpredicted behavior. Even if the app doesn’t crash is that better for a user when the buttons don’t do what they supposed to do, and the information on the screen is misleading? If some internal application invariant is broken, that means the logic is broken as well and the app is not useful anyway.
  • Log and re-throw: developers often add extensive logs but is it a good practice? If the upper layer can handle the exception, that means the logging was unnecessary, and if cannot and takes the same logging approach, it will bloat the logs making the issue harder to analyze. Logging — is the form of exception handling, do it only if it’s appropriate.

Crashing, on the other hand, will let us know about the issue immediately on the testing phase, even before the QA team tries the latest version of the app. And what if the impossible possible and the app start crashing in the users’ hands? We still want to know about it ASAP. All other approached would delay the feedback loop: how long would it take for an average team to figure out that there is a spike of suspicious activity in the logs? How many users would get incorrect bank statements or even worse by that time?

Sample of the legit crashing scenario for Android activity that should be launched via `createIntent`

Track

As you can see, the previous rule only makes sense if we have a feedback loop! Good news that there is a bunch of alerting solutions for mobile and web applications. With proper alerts monitoring in place, we can react to the critical issues in days if not hours, preventing more harm to our users and ourselves.

So the whole strategy is just four rules of thumb:

  • Avoid if possible
  • Recover if not avoidable
  • Pass if possible
  • Crash & Track otherwise

Let your code be exceptionally stable by letting it crash early!

--

--

Software developer. Most recently Android Java/Kotlin engineer. Former manager, desktop and embedded software creator. https://github.com/fo2rist/