Error Handler
The error handling facility in Omega provides a number of macros and an
error class to determine behavior when encountering errors and to provide a
standard error return code that routines can use. Macros are used to provide
a concise single-line interface when checking errors and to ensure the error
message reports the correct line number. It builds on the
Logging facility for writing the error messages and
for some interfaces will generate a stack trace using the
cpptrace library. All interfaces
can be used after including the Error.h
and after initializing the Logging
facility (typically done as early as possible in Omega initialization).
The most common use case is to check for critical errors using the
ABORT_ERROR
macro. For example:
if (condition) ABORT_ERROR("Bad value for variable: {}", Value);
The macro takes a string argument for the error message and allows values
to be inserted where {}
placeholders exist, similar to the Logging macros.
This macro writes the error message and aborts the simulation. If Omega has
been built with debug information available (either a debug build or the
-g option has been passed to the compiler), the macro also creates and writes
a stack trace to provide the calling sequence that generated the error.
Closely related to this critical error macro are the assert and require macros:
OMEGA_ASSERT(Condition, Message, addition args for message);
OMEGA_REQUIRE(Condition, Message, addition args for message);
These emulate the behavior of the C++ assert function. The ASSERT
macro
is only evaluated in debug builds while the REQUIRE
macro is always
evaluated. These have identical behavior to the critical error macro, but
take the additional boolean argument of the condition to be checked.
The remaining macros operate on or with an Error class that contains an error code and an error message. These can be used as a return code, as in:
Error MyError = callMyFunction();
The error code within the class is an ErrorCode enum that defines the severity of the error:
enum class ErrorCode {
Success = 0, /// Standard success code
Warn, /// Standard warning code
Fail, /// Standard failure code
Critical /// Standard critical error code
}
Errors can be accumulated using either the +
or +=
operators. This is
useful especially in test drivers where we may not want to exit when an
error is encountered or if the error is only a warning.
Error MyError; // defaults to success
MyError += myRoutine1();
MyError += myRoutine2();
When adding or accumulating errors, the resulting error code is the max (most severe) error code and the error messages are concatenated (separated by a newline character) so that all messages are retained.
For routines that return an error, we provide an RETURN_ERROR
macro. For
example,
Error ReturnVal; // initializes to success
[do stuff]
if (Condition) RETURN_ERROR(ReturnVal, ErrorCode::Fail, ErrMsg);
This macro accumulates the new error (Code, ErrMsg) into the return code and returns control and the error to the calling routine. The error message is not printed by this macro; the calling routine must decide what action to take with the returned error code and message.
To check these returned error codes or any other accumulated errors in a code, the following macros can be used:
CHECK_ERROR(ErrorToBeChecked, NewMessage, MsgArgs...);
CHECK_ERROR_WARN(ErrorToBeChecked, NewMessage, MsgArgs...);
CHECK_ERROR_ABORT(ErrorToBeChecked, NewMessage, MsgArgs...);
These macros check the error and if it is not Success, prints the new message to the log file along with any other accumulated messages associated with the error. In the first two cases, the error is then reset (back to success with an empty message) and the simulation continues. In the third case, the new message is treated as a critical error, so after printing all the messages, it prints a stack trace and aborts.
While the macros above cover most use cases, it is possible to use the Error class functions directly to customize the treatment of particular errors. For checking error codes, there are two useful member functions:
MyError.isSuccess();
MyError.isFail();
that check whether the code is ErrorCode::Success
or ErrorCode::Fail
.
These functions check for equality so it is sometimes more useful to check
their negation, as in
if (!MyError.isSuccess()) { do something; }
This example will pick up all non-successful conditions (warn, fail, critical).
The default constructor is often used (eg in the examples above) to create and initialize an Error instance for use as a return code. Three other constructors in addition to this default constructor are used by the macros and examples above:
Error MyError; // default constructor with success code and empty message
Error MyError(ErrorCode); // constructor with error code only and empty msg
Error MyError(ErrorCode, ErrorMessage); // constructor with both code and msg
Error MyError(ErrorCode, Line, File, ErrorMessage); // as above but adds prefix
In the last two cases, the constructor initializes the error message either
with or without a prefix. In cases where the message is expected to be
accumulated without an immediate print (eg the RETURN_ERROR
macro) the prefix
case is used and the prefix includes the message severity and the routine name
and line number where the error occurs (the cpp __LINE__
and __FILE__
can
be used). In the other constructor with a message, the prefix is left off,
typically for cases where the relevant LOG function will be called underneath
which adds the approporiate prefix as part of Logging.
In the CHECK_ERROR
macros, the Error is reset to Success and a blank message
using the function
MyError.reset();
Finally, the Error class includes the abort function Error::abort()
which
aborts the simulation. Currently, the function simply prints a generic aborting
message and calls the MPI_abort()
function to kill the simulation, but
additional functionality may be added later to clean up before shutting down.
The abort function assumes it is being called from one of the Error functions
or macros so does not print any previous accumulated errors and does not
print a stack trace. It is best to use one of the ABORT macros above rather
than calling the abort member function directly.