Exceptions are nice. It’s an elegant concept supported natively by many, if not all, mainstream programming languages that help to signal that something went not as planned, and the program can not proceed with execution as usual. However, some people claim it’s an anti-pattern that should be avoided. Let’s take a look at what it’s all about.
The idea of not using exceptions sounds ridiculous. Exceptions make perfect sense; they are easy to learn and easy to use. Why would you abandon such a convenient tool? What would go next: don’t use arrays? Avoid objects? Stay away from polymorphism? But actually, if we would look at places where this idea is proposed we could find that it comes from people who understand a lot about programming: Joel Sapolsky has an article about the topic from 2003, according to C++ style guide exceptions are not used in Google C++ code, and Go (also from Google) doesn’t even have exceptions as a language feature (though technically it does it’s just called differently). So there must be a rational idea behind it.
Let’s create a use case and see what problems we might have while using exceptions.
Alice is working on an enterprise project and adds a new feature. When writing tests, Alice sees that application crashed with the following stacktrace:
Alice’s thoughts about it:
Well, it looks like the app fails if the requested item is out of stock. I guess I should handle this exception at some point, but what would be the best place to add this handling?
ItemManager.reserveItems work? Sure, it makes sense. Order might have a few items, and if other items are available, we might still want to proceed with the order.
OrderProcessor.reserveItems work better? Well, if we’re handling it in
OrderProcessor.processPrepaidOrder would be more reasonable — order has been paid for so we probably should do something with an absent item, e.g. refund or something.
Or we could go with
OrderProcessor.processOrder — then we would handle this kind of exception not only for prepaid orders but for any type of order. We have only one client now for this feature, and they want the order to be all-or-nothing. So let’s go with this idea for now and add handling at lower levels as needed.
Alice adds exception handling to
OrderProcessor.processOrder, the test is now green, and the feature is shipped to production. All is working perfectly fine.
Note that we have to make a decision: at which level should we handle the exception. And it’s not an easy question to answer: there are multiple choices, and all of them seem legit. But let’s be honest: this has nothing to do with exceptions, it’s about software design: who’s responsible for what, what feature we would like to support, and, also importantly, what features we choose not to support at this time.
But there is a more subtle thing that is related to exceptions: while implementing the feature, Alice did not anticipate the error scenario: case when there is no item in stock. If not for the test — the feature would have been likely shipped to production, and users would discover the bug.
Sometime later, Bob, another engineer working on a project, gets an assignment to onboard a new client, which has a slightly different requirement: if some item is out of stock — skip it and continue with the order.
Here is Bob’s reasoning:
All right, the case when the item is out of stock should be handled differently: either whole order should be canceled, or we should skip items and proceed. It looks like the “strategy” pattern would be useful here. Say,
MissingItemHandlingStrategy. We could have it either in
ItemManager.reserveItems, or in
OrderProcessor 's responsibility is to handle orders, so
ItemManager seems to be a better place. It’s “manager” after all, so it should manage this kind of situation.
Then depending on the strategy, we would handle out of stock item differently: either note that this item is not there and proceed or stop altogether. Ok, if we stop — we should probably throw an exception, but it should probably be a new one:
ItemReservationException. Should it be runtime or checked? I guess let’s be consistent with already existing
ItemNotFoundException which is runtime.
Cancel strategy is done, now to the
Skip one. For that, I should probably change the return type of
ItemManager.reserveItems from nothing to list of ids of items we were able to reserve. Done. And also add a strategy as a parameter to our method. And for all existing callers just add
Let’s run the tests and… hmm… one fails. Ah, right, in
OrderProcessor.processOrder we were previously handling
ItemNotFoundException which is now handled at a lower level, so we should change that to
In this example, we see more need for decision-making:
- Should we propagate existing exception or wrap it in a different one?
- If propagate — then at higher levels we should be ready to handle possibly very specific exceptions from way down the call stack.
- If wrap — then we should refactor all code that calls our method to work with the new wrapped exception. And not only direct callers — but also any indirect callers might handle the exception we are touching.
- Also if wrap — should it be a standard exception (e.g.
IllegalArgumentExceptionor something similar) or a custom one? Or maybe custom, but from a list of already existing ones probably residing in
commonspackage of some sort?
- If wrap and create a new exception: should it be runtime or checked? Go with existing choice is good (consistency), but what if both types are used?
- If go with runtime — then the API is not very clear. Yes, we have javadoc to add the documentation, but should we list all possible exceptions that could occur in our class and all its dependencies? Well, that’s a good argument for wrapping things in custom exceptions.
All of that has to be answered somehow. Maybe randomly — creating inconsistency in the project. Or guidelines could be developed, but that would require coordination with other people, meetings, discussions — work that would add no value to the product being built.
Carol is a third engineer on a project, and Bob asked her to review his change.
Let’s see. We have an
ItemManager which has method
reserveItems which now accepts
MissingItemHandlingStrategy and returns list of items that have been processed. If strategy is
Cancel then either all ids would be returned, or
ItemReservationException would be thrown. If strategy is
Skip then only items in stock would be returned.
Ok, but why would we throw an exception for
Cancel strategy. Is it really an exceptional case where some items out of stock? Doesn’t look like it is the case. Why not return something like
ItemReservationResult which would contain a list of processed ids, and a status — was reservation successful or not.
This example illustrates one more additional decision engineer has to make: is it really an exceptional situation or a regular flow of the program? If yes — then exception is the right tool; if no — then return value would be better.
We could sum up the links in the beginning and the example with four points that make exceptions not a trivial tool to use:
- Exceptions allow to focus on a “happy path.” It’s a good thing, but it is so good at this task that with exceptions error handling is often added after the fact. Like a loan it helps to get to an interesting part very quickly, but at a price.
- Exceptions perform a one-way transfer of control somewhere up in the call stack. In this regard, exceptions are a little bit similar to goto, which wikipedia says “performs a one-way transfer of control to another line of code”. As a result, code becomes a bit less predictable — we can’t say how the program would execute just by looking at function code.
- In certain cases exceptions are just return values. It is an inappropriate use of exceptions, but it’s not a trivial task to come up with the test to check if the use of exception is appropriate or not (so in a sense it’s not an NP problem). Especially in situations where it was a proper choice at first, but then, as always, something has changed, and it’s no longer the case.
- Is it easy to do the right thing and hard to do the wrong thing? No, quite a good guidelines are required to use exceptions properly. And not only guidelines — machinery to enforce it should be put in place. So a considerable amount of work should be done, which is completely unrelated to the project itself.
What is the alternative?
Treat errors as values. There are a few examples of this approach:
- Scala’s functional error handling
- Google’s C++ Status(Or) with RETURN_IF_ERROR and ASSIGN_OR_RETURN macros
- Go’s returning err and then
if err != nileverywhere
So exceptions should never be used?
Well, actually, no.
Yes, in this article we’re looking at difficulties while using exceptions. But! Some of the things discussed here apply both to exceptions and “errors-are-values.” Also, in some cases, this goto-feature of exceptions is quite handy.
This topic is explored in the next article: