Error Handling with Exceptions: Where to Catch
By Dave Brownell | November 16, 2010
This article is part of a longer running series on Error Handling with Exceptions.
Determining where to catch an exception can be one of the most difficult aspects to overcome in becoming comfortable with exceptions. These rules provide clear guidance and simplify the process by removing ambiguity.
Please note that this article’s scope is limited to the location of catch clauses. Questions on the state of the application and its ability to continue in the presence of specific exceptions will be addressed in future articles. The distinction between where to catch and what to do when an exception is caught is important, as this represents one of the most misunderstood facets of effectively working with exceptions.
Where to Catch
- Catch specific exceptions locally if it is possible to mitigate the error and continue
- Catch all exceptions locally if the error is benign within the context of a function’s intended purpose
- Catch all exceptions at the function level when wrapping exceptions
- Catch all exceptions at logical boundaries
Catch specific exceptions locally if it is possible to mitigate the error and continue
Although relatively uncommon, it is possible to automate the correction of an error condition and continue execution. For instance, a function that opens a file may be in a position to prompt the user and wait if an exception associated with a file in use is encountered. In these limited scenarios, it is appropriate to catch specific exception(s), correct the problem, and continue.
void MyFunction(void) { while(true) { try { AFileHandle hFile; hFile = OpenFile(...); // Process file... break; } catch(FileInUseException const &ex) { PromptUser(); } } // Continue... }
Catch all exceptions locally if the error is benign within the context of a function’s intended purpose
There are times when an exception encountered within a function is benign within the context of the function itself. For example, failure to write to a log file may be critical when used for auditing purposes as part of a financial transaction but benign within the context of a function that is initiating emergency shutdown of a nuclear reactor.
It is important to reiterate that at this point in the discussion, it isn’t important to discuss what exception was thrown but rather where it was thrown and what that failure means within the larger context of the function.
Any time a function is called within a function that we are writing, we should ask ourselves “what does a failure here mean to the calling function”. In most cases, failure in a called function indicates failure for the calling function – we should not catch and let the exception percolate. However, if executing the calling function is far greater in importance than successfully completing the called function, we should catch and let the calling function continue.
// Note that this code is not production-quality and is // intended for illustrative purposes only. // Logging is critical to the function's success void DoBankTransaction(void) { Log::Write("Begin transaction"); // Do transaction Log::Write("End transaction"); } // Logging is not critical to the function's success void BeginEmergencyShutdown(void) { try { Log::Write("Begin emergency shutdown"); } catch(...) { /* Log exception */ } // Do shutdown try { Log::Write("End emergency shutdown"); } catch(...) { /* Log exception */ } }
Catch all exceptions at the function level when wrapping exceptions
Exceptions should be caught and converted when implementing a function whose contract specifies that only exceptions of a specific type will be thrown. There are times when it can be convenient to specify that all exceptions that originate from a specific layer within an architectural hierarchy be of a consistent type (note that this convention becomes significantly less important when leveraging techniques to be outlined in “Generic Customization of Exception Messages”). In these cases, all exceptions should be caught and converted within the function itself. Care should be taken to ensure that the new exception thrown contains a reference to the original exception, as this is required when investigating the root cause of the problem while post-mortem debugging.
// Note that all exceptions eminating from this function // will be of type MyFunctionException. void MyFunction(void) { try { // Do something that may throw } catch(PotentialBaseException const &ex) { THROW_EXCEPTION(MyFunctionException(ex)); } catch(std::exception const &ex) { THROW_EXCEPTION(MyFunctionException(ex)); } catch(...) { THROW_EXCEPTION(MyFunctionException()); } // This code is painful to write and maintain and is not // recommended. See the article "Error Handling with Exceptions: // Generic Customization of Exception Messages" for an // alternative. }
Catch all exceptions at logical boundaries
All exceptions should be caught at logical boundaries, where a logical boundary may be an application’s entry point, subsystem boundary within a partitioned architecture (eg the boundary between a COM component and C++ layer), binary boundary, programming language boundary, or a “home screen” within a GUI application.
Leave Your Comment