Using Exceptions For Program Control Flow?
With my recent article on when to use exceptions, I got a few questions on whether we should or shouldn’t use exceptions as control flow mechanisms. Many of us are taught early in our careers not to use exceptions for this purpose.
Now, it might be worth our while to delve into what this means.
Exceptions, by their very nature, change the flow of a program, not unlike a return statement. Whereas a return statement terminates the execution of the current function and returns control to the function’s caller, throwing an exception unwinds the stack until it is caught or the program ends with an error code.
So, exception flow behaves like an in-flight return statement that can be ‘returned’ to any of the upstream callers. Well, not returned, but caught. And the catching code, then, may be the immediate calling function, or it can be the caller of that function, and so on, all the way up the program call stack, if needed.
As discussed in When to Use Exceptions, we want to use such a powerful escalation process only for problems we cannot handle locally. Said another way:
Don’t use exceptions for the ‘happy path’.
Here is an example of how NOT to use Exceptions:
private async Task<CompletedAccountTxList> CalcAndSaveNewCompletedTransactions( OrderedAccountTransactions savedTx, OrderedAccountTransactions bankTx) { try { CalcNewCompletedTransactions(savedTx, bankTx); return new CompletedAccountTxList(); } catch(NewCompletedTransactionsException ex) { await SaveNewCompletedTransactions(ex.TxList); return ex.TxList; } }
What’s going on?
Helper method CalcNewCompletedTransactions() throws a NewCompletedTransactionsException exception containing the list of transactions. Resultant transactions are then saved via the SaveNewCompletedTransactions() method. Apparently, we are looking at the happy path flow being implemented with exceptions! That is suboptimal and different from what we want.
Why?
We can control the flow of the program with more straightforward and effective language structures, like a return statement and an if conditional. Let’s try again, but this time, CalcNewCompletedTransactions() directly returns the data:
private async Task<CompletedAccountTxList> CalcAndSaveNewCompletedTransactions( OrderedAccountTransactions savedTx, OrderedAccountTransactions bankTx) { var newTx = CalcNewCompletedTransactions(savedTx, bankTx); if (newTx.Any()) await SaveNewCompletedTransactions(newTx); return newTx; }
Isn’t that so much simpler to understand? We’re calling helper method CalcNewCompletedTransactions() to figure out the collection of transactions, which it returns. If there are transactions, we save them. Done.
Conclusion
So, what we’ve learned is true: We should NOT use exceptions as program control flow mechanisms for the ‘happy path’. On the other hand, we DO want to throw exceptions to escalate situations beyond the scope of the current function.
Leave a Reply
Want to join the discussion?Feel free to contribute!