To Throw or Not To Throw

Vineeth Venudasan
CodeX
Published in
4 min readOct 4, 2020

--

I often found teams convincing themselves that they don’t do flow control using exceptions because they don’t do constructs like a ResultException as described in Ward Cunningham’s wiki.

Things may not be that bad in most of these teams.

But I’ve seen exceptions being used in ways that are very close to flowing control, especially when the presence of said exceptions are used to trigger complex business scenarios.

You might have also seen that this indeed an oft-used method, so what is so bad about doing using exceptions this way?

1. Exceptions can increase cognitive-complexity

Rampant use of exceptions in your code-base can make it difficult to read and understand. Remember that every catch increases cognitive complexity.

My personal preference when it comes to exceptions was always this: exceptions should be used only when something exceptional happens. In other words, throw an exception only when you need to make a fast exit from something bad that has already occurred.

A quick exit from the building [Image by Aintschie from Pixabay]

For instance, if the business function being executed is one to capture a payment, and if some dependency required to fulfill that function is unavailable (like the external payment gateway is down) — throw an exception.

Another solution would be to avoid exceptions altogether, and choose another mechanism like a Result object or an Either monad

However, I’ve not seen every team eager to adopt a new concept like a Result object. Also, consider that it’s not very easy to migrate existing code.

So this got me thinking. Is there a set of guidelines that we could follow that make the code better readable while still using exceptions?

As such, I tried to prepare a list.

Basic Guidelines

1. Handling Exceptions from external libraries

Handle exceptions thrown from third-party libraries close to where they might be thrown. Do not handle that exception further downstream.

If we let an exception thrown by a third party propagate to all layers of your code, it increases the coupling to that library.

This can also raise subtle, uncaught errors when you switch the library to another one if it is ever required.

2. Respect architectural boundaries

This is fairly similar to the problem above, but I mention it as a separate point because it is something that I have seen so often.

For example, avoid handling a database-specific exception thrown from the database layer in the presentation layer.

3. Handling your own exceptions

If you throw an exception in your code yourself, handle it just once.

This should be done only in one location — hopefully, a dedicated ExceptionHandler class, if you come from the Spring world.

The reason for this advice is due to the very nature of exceptions, that they can function as non-localized goto statements.

As such, I would like to downgrade the problems it can raise, by having a single place to look at where an exception is handled. For a future maintainer, this is easier than having to go through the complete execution chain to understand what happens.

This paradigm might raise some interesting problems to solve too.

We often need to catch exceptions that we have thrown ourselves, maybe update a metric or two, or a database entity, and re-throw the same or (even) a different exception so that some code downstream can handle it.

There are ways to do the above things that might involve not using exceptions in the first place, or even revising code architecture so that you satisfy the paradigm that you handle your thrown exceptions just once.

That often is not easy, but it will pay off in the future when the next person tries to read and decipher what is happening.

4. Document API methods

If you are writing a method that throws an unchecked exception, please document it. Simple JavaDoc comments will do the trick. A method that does not document the exception(s) that it throws lies about its contract.

Help the future maintainers of your code. If they do not have this documentation, it could be that have no idea that it throws an exception in the non-happy path case.

What usually follows is that they either

  • wrap all the code calling this method with try and catch blocks, making the code unnecessarily verbose (or)
  • forget to catch the exception, and might end propagating the exception to a downstream layer.

Both, bad places to be in.

Conclusion

These were fairly basic points that came to my mind while working on a recent project and thinking about ways to write more maintainable and easily readable code.

References

  1. What is cognitive complexity — https://www.sonarsource.com/docs/CognitiveComplexity.pdf
  2. Monads in Scala — https://www.freecodecamp.org/news/a-survival-guide-to-the-either-monad-in-scala-7293a680006/
  3. Kotlin Result Object — https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/

--

--

Vineeth Venudasan
CodeX

Backend Engineer / Software Consultant. The views expressed here are my own and does not necessarily represent my employer’s positions, strategies or opinions