Project

General

Profile

Error-handling policy

The subject of error-handling is broad and context-dependent. Here we highlight considerations that are important when using the art framework. For general error-handling guidance, we refer users to the Error Handling section of the C++ core guidelines, which users to adopt.

Use of assert vs exceptions

Within the context of an art job, when an exceptional condition is encountered within user code, the user has to decide how to deal with that exceptional condition.

The number one reason for excluding asserts from production code is that every experiment consulted when putting together requirements for art stated that it was a priority to avoid data loss wherever possible. Given that, the developer cannot be the one to decide which errors are fatal and cause immediate termination.

There are several other good reasons to prefer exceptions over asserts in the context of an art job:

  • With modern compilers there is no runtime cost associated with exceptions unless the exception is actually invoked.
  • In debuggers you can learn more about the state of the program at a trapped exception than you can with a terminated program (in GDB for example, use catch throw followed by, where when execution is halted).

However, the first-mentioned reason is overriding: the experiment needs to control the program, not the developer. An experiment should have a policy on the correct use of trappable exceptions, and it needs to be enforced in all code against which you link (including LArSoft and Nutools, for instance): data will be lost if an assertion fires. If a cet::exception or cet::coded_exception is used, the behavior of the framework with respect to that particular exception may be controlled at runtime. In all cases though, the framework will make every effort to close the currently-open output file(s) and exit gracefully. Upon a terminate (as caused by a failed assert), an output ROOT file is almost guaranteed to be unreadable.

However, this is not to say there is no case where asserts are useful. In our opinion, a useful policy would be:

Asserts should only be used while developing code to verify that a function's preconditions1 or postconditions2 are satisfied. If something can go wrong based on program input (physics data or user configuration, say), then the behavior should be to throw an exception, not abort().

1 Preconditions are those things that a function can assume are true upon entry. operator[]() for a standard container, for example, assumes that the provided index is within range (with the resulting horrifying consequences if the precondition is not met); at() does not, at the cost of increased execution time for the function as compared to operator[]().

2 Postconditions are those things that a function guarantees are true upon exit from the function.

When production code is compiled with -DNDEBUG, the cost of evaluating those preconditions goes away with the asserts. This means, of course, that an assertion expression should not have side effects! For example, consider the following (not very safe) code:

class A {
public:
  bool init() { a_ = 5.0; return true; }

  doStuff() { std::cout << 5.0 / a_ << std::endl: }

private:
  double a_;
};

int main()
{
  A a;
  assert(a.init());

  a.doStuff();
}
When an A is constructed, its double data member a_ is not initialized. If -DNDEUG is used for production code, a.init() is never called and a.soStuff() has a significant chance of causing a runtime error. The safest version of this strawman code is of course one that does not need a separate init() function, with the work done in the constructor. However, the minimum change needed to remove the deleterious effect of the assert would be:

bool status [[maybe_unused]] = a.init();
assert(status);

instead of the assert on line 14 above. Note though, that even though this code is no longer technically wrong, we are still not following our own advice regarding the use of asserts: if it is possible, under normal operating conditions in production, for a.init() to fail, then we should throw an exception rather than assert(status).

The converse rule, for completeness, is that exceptions should not be used for conditional flow control: reserve them for, "exceptional" conditions. Use standard flow control tools for flow control. Debugging a "real" exception condition gets hard if the code is always throwing exceptions and catching them to deal with minor variations in normal execution.