I was reminded today about a discussion I have had time and time again about when it is appropriate to use exceptions instead of return codes. This often comes up in serious development discussions.
In order to better understand my stance on it, we have to all understand one very important thing about exceptions (talking about C++ and similar constructs here): Exceptions Are Destructive.
Destructive? Yes, destructive.
The major difference between a return code and an exception is that an exception causes a stack unwind to happen. Now, I can hear some of you out there saying: “Well, DUH!” Yes, it might seem obvious at first, but consider the implications of it. Where is the exception going to be caught at? Will it be caught so many levels above where it occurred that the context of what was going on is completely destroyed?
I have rarely seen code that uses many nested try/catch blocks such that exceptions are caught (and handled!) close to the point where they occur. Have you ever seen single function calls wrapped in their own try/catch block? More often than not, an entire function is wrapped in a large try/catch block that either “eats” any exceptions thrown, or just re-throws them up the chain. This practice makes “contextually-appropriate” error handling a lot more difficult. So we will first look at handling exceptions. For example, consider the following code snippet from a translation engine:
try { CBookData bdBookData( TheBookData ); TranslateTextToRussian( bdBookData ); TranslateTextToElbonian( bdBookData ); TranslateTextToGerman( bdBookData ); TranslateTextToBritishEnglish( bdBookData ); } catch( CTranslateException &exXLate ) { MessageBox( m_hWnd, exXLate.GetDescription(), _T( "Failed To Translate Text" ), ( MB_ICONEXCLAMATION | MB_OK ) ); }
Now, think about failure recovery for a minute. This application appears to translate some arbitrary text into a few different languages. Suppose the the call to TranslateTextToGerman
() fails because it cannot translate the word “cuz” (which was supposed to be “because”) and it throws an exception. At this point, we have already successfully translated the text into two languages, but the “context” of that progress is lost when the exception is thrown. Imagine that this code is translating a 2000+ page novel and takes ~4 hours per language. Would you want to lose ~8 hours of work because of a typo and poor code design?
Granted, part of the problem is the poor design of the code calling the functions. But I find that this kind of coarse-grained exception handing is more common than not. Languages that enforce exception handling can actually make the problem worse! To avoid errors, developers will sometimes wrap huge sections of code in a single try/catch block, even if those sections of code contain separate or unrelated functionality, thus making the exception handling even more granular than before.
Now, onto throwing exceptions… IMHO, throwing an exception should only be considered in situations where the function/code cannot complete the job it is designed for. Take the following example; imagine a function in a compiler that does the following steps:
- Parse language-specific code in a file into compiler’s intermediate language (IL) saved to a temp file
- Compile the temporary IL file into native executable code in a new EXE file
- Delete the temporary IL file
Using exceptions, you can throw an exception at any one of the three steps. However, only a failure at the first two steps is critical such that no executable could be generated – the job of the function could not be completed. If the temporary file could not be deleted, then fine – we can clean it up later. But we still have the EXE generated, which was the actual job of the function! Hell, even if we did leave the temporary file lying around somewhere, the job still got done to the point where usable results were generated!
When using exceptions for everything, the only way to handle these kind of situations is to wrap each and every function call with its own try/catch block, and/or being sure to catch all possible types of exceptions, and reporting on them accordingly. This is much more work than just doing a “catch all” at the bottom to catch really serious problems (like disk hardware errors). Not the most ideal coding situation, eh?
With return codes, it would be trivial to continue past the point of failure of an individual function call, because you do not have to take steps to preserve the context of what is going on. In the first code sample above, you could easily note that one of the function calls failed, and then continue on with the rest of ’em, being able to perserve any work that did completed successfully. Think of a “success with warnings” status code being returned. Even if you created a “success with warnings” exception type, you would be putting the onus on the developer calling you code to correctly handle this special exception and determine if things should continue or not.
In short, use exceptions for serious problems, not for normal error situation handling, or to report on just plain status.