There’s been an never ending debate of whether assertions should be converted to exceptions — if an assertion check is useful in Debug build, why remove them in release build? There are some cases where you want to do extensive checking (for e.g. walking over the whole array and detect data corruption) and it’s fine to remove those extensive slow checks during production, but most of the time, the performance difference should really be negligible.
4 kinds of strategies to handle broken assumptions
To fully understand the problem domain, it is useful to clarify the concepts. From my understanding, the strategy to handle broken assumptions can be categorized as follows:
This is what you get with
assert in most languages or Debug.Assert in C#.
- The most defining characteristic of cassert is that it is removed in production. - In theory this can give you a slight performance gain. I find that it’s not useful most of the time — unless you’re dealing with very low level stuffs. - If you use `cassert`, you assume that **a violation to your assumption is not going to cause catastrophic consequences**. Personally, I find that is a very bold assumption to make. It is basically trying your luck — after violating the assumption, your program has entered undefined behavior land. Allowing the program to continue could result in data loss or the end of the world. - This is definitely not suitable for mission critical programs where everything must have clearly defined behavior.
I heard this idea was introduced in Code Complete. The basic idea is that you still have the checking in release build. If the assumption is broken, you write it to a log and allow the program to continue.
- The reason behind is that there is no programmer to fix the bug when the assumption is broken, so there is no point showing the error message box to the user. The program can do nothing to fix itself, so it may as well continue. The logging bit is there to help post-mortem troubleshooting. - I personally find that it is futile to log things because it rarely gives you enough information to diagnose the problem. Unless you dump the current stack-trace and local variables in the log, a message like “object should not be null!” is not going to help much — you may as well collect a crash dump. If your program is an input-output program, you may as well ask the client to send the input so you can try to reproduce it in a debugger — where you have such a wealth of information at your disposal. - Continuing after logging suffers the same problem as `cassert` — your program’s behavior is no longer defined. As such, it should not be used in any reliable or mission critical software.
The Terminator basically means calling
abort() in case of a broken assumption. Alternatively, you can call a elaborately designed termination routine that has been rigorously tested.
- The compelling reason to use this strategy is that it’s closest to 100% well defined behavior. Nothing funny should be able to happen. This is particularly desirable if one wrong instruction in your domain can launch a missile or stop a patient’s life support. - This is also suitable in unstable environment where a broken precondition could be the result of a full heap corruption. - The Terminator is really the _correct_ solution in terms of theoretical, academic discussion. However, it is also the most inflexible solution in terms of writing useful software — terminating the program with an error dialog box is not the nicest thing to do in modern consumer software.
Finally we have good ol’ exceptions.
- Stack unwinding and the ability to handle them are the defining features of exceptions. - Exceptions stops execution of the current routine immediately — this means if you use exceptions in a function, your function is guarded against any undefined behavior (provided you fully defined exceptional cases). The responsibility to maintain integrity of the program is now shifted to the exception handler — the outside `try…catch` block. - Exceptions also offer a high level of flexibility in handling errors — the actual error handling strategy is implemented by your caller. You could call it some sort of [Inversion of Control](https://secure.wikimedia.org/wikipedia/en/wiki/Inversion_of_control), which is usually a nice thing in terms of architecture. - One potential security hole remains: stack unwinding. Unwinding the stack would call the destructors on local variables — which can do anything. - From an extremely rigorous perspective, exceptions aren’t really safe. Your broken assumption could be the result of a full heap corruption, in which case it is really not advisable to allow further code execution in stack unwinding. There is also the fact that your caller could ignore the error in a catch block.
So what does it mean?
It means you should always roll your own because there are different needs for different people.
- A mission critical software must have clearly defined behavior and flexibility in error handling is not something they need. They would probably use a variation of the Terminator pattern. - A video game would probably benefit from cassert because they need every last bit of performance they can get, and they can reasonably believe that their audience’s platform will not launch a missile. On the other hand, video games should “push their luck” in the face of broken assumptions — the show must go on. If the game can still reasonably continue after a failed assertions, it should. (Exercise to reader: how about the game saving part? Probably the most devastating thing a game can do is to delete or corrupt your game saves.)
The take home message
I would suggest people should always write their own strategy to handle broken assumptions. Personally I like to start with something called
ENSURE which implements exception style error handling by default. The good thing about a hand rolled solution is that you can always change your strategy later. For a few places where you’d like extensive checking, you can use