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 should be considered authoritative unless it conflicts with the guidance on this page, which includes considerations specialized for our environment.

Be consistent

Using a mixture of error-handling techniques without forethought can lead to confusion, suboptimal code, and sometimes wrong code. Each experiment should have an error-handling policy that is used consistently within its own code base, taking into account the (possibly different) error-handling policies of third-party libraries.

Use exceptions for errors only

Although this guidance is already given in the C++ core guidelines, it bears repeating:

Exception-throwing and -handling should never become part of the routine execution of a program.

Consider the following example which exhibits a fairly common, and suboptimal, pattern seen in some code:

// Confused attempt at error-handling
void MyProducer::produce(Event& e) {  
  try {
    auto h = e.getValidHandle<MyType>(...); // Might throw
  }
  catch (...) {
    std::cerr << "Data product of 'MyType' not found.\n";
    return;
  }
}

Such a pattern suggests an ambiguity regarding how the author intended this code to function:

  • Should failure to retrieve the product be an error (as suggested by using the 'getValidHandle' function, which may throw, instead of the 'getByLabel' function)?, or
  • Is it acceptable for the program to continue if a product is not found (as suggested by the 'catch')?

Presumably it is the latter, otherwise there would be no try-catch block in the first place. There are two solutions to this problem, depending on the intention of the author.

When failure to retrieve a product is an error

If the code in question cannot or should not be executed when a product cannot be retrieved, then it is sufficient to just make the 'getValidHandle' call:

void MyProducer::produce(Event& e) {  
  auto h = e.getValidHandle<MyType>(...); // Might throw, let the framework handle it
}

In this case, the framework will take care of the exception-handling, following the configuration directions regarding what the appropriate action should be when a product lookup fails.

When failure to retrieve a product is not an error

In cases when a product may be expected to be absent, then a non-throwing function should be called to retrieve the product:

void MyProducer::produce(Event& e) {
  Handle<MyType> h;
  bool const success = e.getByLabel(..., h);  // Returns 'true' if product found
  if (!success) {
    std::cerr << "Data product of 'MyType' not found.\n"; // Consider suppressing these types of messages
    return;
  }
}

Choosing either option above (neither of which has a try-catch block) is unambiguous in terms of the intention of how the code is to be used, is easier and clearer to read, and thus more maintainable. In addition, 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.

Use of asserts vs exceptions

When putting together requirements for art, every experiment consulted stated that it was a priority to avoid data loss within the context of an art job. Therefore, the developer--i.e. author of code to be used in a framework job--cannot be the one to decide which errors are fatal and cause immediate termination. Such requirement has led to the following guidance:

To avoid data loss where possible, asserts should be excluded from production code.

Data will be lost if an assertion fails. 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 configuration time. In all cases though, the framework will make every effort to close the currently-open output file(s) and exit gracefully. Upon an abrupt termination (as caused by a failed assert), an output ROOT file is almost guaranteed to be unreadable.

There are, however, cases where the use of asserts is appropriate. A useful policy is:

Asserts should only be used to verify that a function's preconditions1 or postconditions2, or a class's invariants3 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.

Use of asserts and side effects

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.doStuff() 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).


1 Preconditions are criteria that must be satisfied upon entry into the function in order for the function to execute correctly. A failed precondition is a logical error that can and often leads to undefined behavior in the execution of the program. For example, operator[]() for an STL container assumes that the provided index is within range (with the resulting horrifying consequences if the precondition is not met); at() does not make such an assumption, at the cost of increased execution time for the function as compared to operator[]().

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

3 Invariants are features of an object that, as perceived by the user (from outside the class), must remain true during the lifetime of that object.