Sugioarto

2015-02-16

Assertions and Fault Tolerance

The quest for software quality is important and bugs are something that you can expect in code, but are unexpected when encountered during runtime, because they are simply not handled. Here are some thoughts about software design that I encountered during doing my daily job improving software quality.

The Case For Assertions

I usually use assertions a lot to quickly find bugs in software by inspecting invariants, pre- and postconditions. This works well and makes projects more stable and robust. While crashing obiously and visibly, it leads to improvement and reduction of such situations in the long run.

Assertions are simple like this:

assert(condition_that_has_to_hold, "message to output when it does not hold");

Usually assertions are implemented in a way in which they can be removed from production code. While this is ok to allow, it is generally a bad idea for common software. A typical assertion notice that is in production code are the famous blue screen of death and other kinds of kernel panics.

Assertions Are NOT Exceptions

Many people confuse an assertion with an exception. An exception is not a fatal situation and especially it does not mean that the program has a bug. An exception is to be handled and the program can be running again.

When a program hits an assertion, it is obvious that it does not make any sense to keep it running, because something fatal happened and it will never reach any reasonable state to keep operating. For example it is to expect that a side-effect will remain in the program and it won't work, even after repeating a procedure, it can behave wrong in all possible routines. That said, it is important the the system is resetted and put in a clean state.

Exceptions, Like Assertions, Are Unexpected

Many people throw exceptions for obvious and expectable things, or, even worse, they control the regular (non-exceptional) program flow using them. Both uses are wrong.

For example, it is expected that a file has an end somewhere. You do not need to throw an exception here, because this is quite obvious that a file will have an end (eventually). It is rather exceptional, if it does not have an end, which can happen, if it is circular due to a filesystem inconsistency or, perhaps, an exotic filesystem (that I have not seen, yet). So the test for EOF (end-of-file) should never signal the EOF condition with an exception, but with a return value.

I have seen people programming a recursive function that does not let an exception unhandled, but uses it to signal special cases which it returns results with. This is a typical case for using exceptions for controlling program flow. When a valid and expected result of a method or function is an exception, it is not a good idea (in most cases) to use exceptions, but simply return a special value. Also in many languages exceptions are slow, so do not make your routines unnecessarily slow.

Assertions Are NOT to Be Used For Fault Tolerance

Assertions are not fault tolerant. And it is OK. Assertions should reset the system and a fault tolerant system must be aware that a reset of a component or the entire system happened. Fault tolerance is planned and it is expected that a part of a program can have a valid result marking a fault of a component.

Assertions are something different. Assertions are used for bugs, that means for cases when the program encounters an unspecified behavior. Bugs are never expected, because they are cases which the program, even they are allowed does not handle correctly. Fault tolerance is the mechanism to handle eventual specified and well-known bad behavior that is not a bug.

In recent past I have seen code like this:

if (!condition_that_has_to_hold) { assert(false, "message to output when it does not hold"); return; }

What is happening here? It is usually some code that is used to check preconditions and trying to fail "safely" from a fatal condition.

Remember, assertions are used for fatal and irreparable situations and this is code that safely returns from this fatal situation introducing many problems in various places later on, which you cannot foresee as a programmer.

Such code tries to combine fault tolerance with bug discovery. It seems allright at a first glance, but fault tolerance is only useful when the program can return to a state in which it operates safely and bug-free. A triggered assertion signals a bug and you would be lying to yourself to try to recover from a bug that you have not handled. Be aware that this is not fault tolerance but hiding a bug (or "masking" it).

A proper approach is to separate these two different aspects. It might look like this:

if (special_handled_condition) { log_condition(); if (try_to_fix_situation()) return SPECIAL_CONDITION_FIXED; } assert(special_handled_condition, "special condition could not be fixed");

The call to log_condition is sometimes expected to fail. You need to decide yourself, if logging (which is still useful here) is still possible or it is not. If it is not, it is better to place the log_condition call in the if statement branch right before the code returns.

Also note that after fixing the situation it might be ok to carry on with the procedure instead of returning. You need to decide yourself what is reasonable and what is not for your particular case.