Error Handling with Exceptions: What to Throw
By Dave Brownell | November 3, 2010
This article is part of a longer running series on Error Handling with Exceptions.
Fortunately, there are only a couple of things to keep in mind when throwing exceptions (and most of these requirements are built into the language or codified through convention).
What to Throw
- Exception types should be specific to an error
- Exception types should derive from a common base class
- Instances of exception types should contain all information to accurately diagnose the conditions that cased the failure
- Instances of exception types should contain all information to accurately diagnose where the exception was thrown
Exception types should be specific to an error
There should be a 1-to-1 correspondence between exception types and the error that caused the exception to be thrown, such that it is possible for a caller to catch on a specific problem (e.g. an attempt to open a file that is locked by another user) or a more general problem (e.g. an attempt to open a file failed). In some cases, it might be appropriate for a caller to initiate action to correct the error that caused the exception to be thrown, while in others the caller may choose to terminate the current operation and restore the user to a known good state. The end result is that it isn’t the thrower than knows how to continue, it is the caller – and that caller needs the correct contextual information to make informed decisions.
Some languages and platforms place cumbersome criteria on what it considers to be a valid exception type. For example, the following is a minimal C# exception type:
[System.Serializable] public class MyException : System.Exception { public MyException() { } public MyException(string message) : base(message) { } public MyException(string message, System.Exception inner) : base(message, inner) { } protected MyException( System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context) : base(info, context) { } }
In such systems, a foundation should be created (through macros, code snippets, base class implementation, etc) to reduce as much friction as possible when creating new exception types. Any friction not removed will serve as a deterrent to creating exception types specific to the errors that they represent.
Exception types should derive from a common base class
In the vast majority of cases, custom exception types should be a part of well-defined exception hierarchy ultimately rooted in an object defined by the programming language or common library. Exception types not conforming to this recommendation greatly increase the burden placed on the caller by requiring additional catch logic for incongruent types.
For example, in the following code example MyException1 and MyException2 are not based on std::exception, thus placing a requirement on the caller of FunctionThatMayThrow to handle inconsistently rooted exceptions.
struct MyException1 {}; struct MyException2 {}; try { FunctionThatMayThrow(); } catch(MyException1 const &ex) { // Do something with ex } catch(MyException2 const &ex) { // Do something with ex } catch(std::exception const &ex) { // Do something with ex }
Even worse than the burden placed on the caller is that this code is brittle; what happens when FunctionThatMayThrow or a function that it calls is modified such that MyException3 is thrown? Without a common bases class, all exception-aware functions using FunctionThatMayThrow (either directly or indirectly) must be modified to handle MyException3.
The following addresses these problems in that the exception types are based on a well-defined object hierarchy. As a result, the caller can catch by exceptions of a specific type, exceptions in a common hierarchy, or any exception that is thrown.
struct MyBaseException : std::exception {} struct MyException1 : MyBaseException {}; struct MyException2 : MyBaseException {}; // Handle all exceptions try { FunctionThatMayThrow(); } catch(std::exception const &ex) { ... } // Handle MyBaseException and children try { FunctionThatMayThrow(); } catch(MyBaseException const &ex) { ... } // Handle specific exception try { FunctionThatMayThrow(); } catch(MyException1 const &ex) { ... }
Instances of exception types should contain all information to accurately diagnose the conditions that cased the failure
Exceptions should contain enough information to determine what happened to cause the exception.
When post mortem debugging, an exception is oftentimes the only information available to help diagnose the problem on a remote machine. In the absence of a consistent repro scenario or the ability to attach a debugger, the exception must be complete and contain all information necessary to determine what happened. This information may include the name of a file that couldn’t be opened, the name of a socket address that was unexpectedly disconnected, or the name of the variable that was null in the case of a null reference exception. I have found the answer to the following question serves as a good guide in fulfilling this requirement:
Given this exception and nothing more, it is possible to determine exactly what went wrong?
Instances of exception types should contain all information to accurately diagnose where the exception was thrown
Exceptions should contain enough information to determine where a problem originally happened. In most cases, determining where an exception happened means capturing and persisting call stack information at the moment the exception is thrown. This information should be a part of every exception and available when the exception is caught (regardless of where the it is caught or if the exception is wrapped by a new exception).
Many languages and platforms make it very easy to capture call stack information while others do not. At the time of this writing, the following resources are available to generate call stack (or call stack-like) information in C++ applications:
- __FILE__, __LINE__, and __FUNCTION__ preprocessor macros
- Walking the Callstack by Jochen Kalmbach
- [boost] [Backtrace] Any interest in portable stack trace?
Leave Your Comment