Error handling service

An error handling service is provided encapsulated in a singleton object (an instantiation of the Errors class).

The Errors service provides global error handling functionality.

Errors are defined in the error catalog in ErrorCatalog.h (see ERROR_CATALOG).

Errors defined in the error catalog have a scope and message text. The scope is used to determine if and when an error should be printed.

The current values for scope are:

NEVER

the error will not be printed.

ALWAYS

the error will always be printed.

FIRST

the error will be printed only on the first time it is encountered anywhere in the program in the current execution of COMPAS.

FIRST_IN_OBJECT_TYPE

the error will be printed only on the first time it is encountered anywhere in objects of the same type (e.g. Binary Star objects) in the current execution of COMPAS.

FIRST_IN_STELLAR_TYPE

the error will be printed only on the first time it is encountered anywhere in objects of the same stellar type (e.g. HeWD Star objects) in the current execution of COMPAS.

FIRST_IN_OBJECT_ID

the error will be printed only on the first time it is encountered anywhere in an object instance in the current execution of COMPAS (useful for debugging).

FIRST_IN_FUNCTION

the error will be printed only on the first time it is encountered anywhere in the same function of an object instance in the current execution of COMPAS (i.e. will print more than once if encountered in the same function name in different objects; useful for debugging).

The Errors service provides methods to print both warnings and errors – essentially the same thing (for printing), but warning messages are prepended with 'WARNING: ', whereas error messages are prepended with 'ERROR: '.

Errors and warnings are printed by using the macros defined in ErrorsMacros.h. They are:

Error macros

SHOW_ERROR(error_number)                         // prints the error message associated with error
                                                 // number (from the error catalog) prepended by
                                                 // 'ERROR: '

SHOW_ERROR(error_number, error_string)           // prints the error message associated with error
                                                 // number (from the error catalog) prepended by
                                                 // 'ERROR: ', and appends 'error_string'

SHOW_ERROR_IF(cond, error_number)                // if 'cond' is TRUE, prints the error message
                                                 // associated with error number (from the error
                                                 // catalog) prepended by 'ERROR: '

SHOW_ERROR_IF(cond, error_number, error_string)  // if 'cond' is TRUE, prints the error message
                                                 // associated with error number (from the error
                                                 // catalog) prepended by 'ERROR: ', and appends
                                                 // 'error_string'

Warning macros

The WARNING macros function in the same way as the ERROR macros, with the exception that instead of prepending the message with 'ERROR: ', the WARNING macros prepend the message with 'WARNING: '.

The WARNING macros are:

SHOW_WARN(error_number)
SHOW_WARN(error_number, error_string)
SHOW_WARN_IF(cond, error_number)
SHOW_WARN_IF(cond, error_number, error_string)

Static macros

An additional set of macros is provided to be used in static functions and other functions that are not contained within an instantiated object (e.g. main()).

The static ERROR macros are:

SHOW_ERROR_STATIC(error_number)                       : prints "ERROR: " followed by the error message associated with "error_number" (from the error catalog)
SHOW_ERROR_STATIC(error_number, error_string)         : prints "ERROR: " followed by the error message associated with "error_number" (from the error catalog), and appends "error_string"
SHOW_ERROR_IF_STATIC(cond, error_number)              : if "cond" is TRUE, prints "ERROR: " followed by the error message associated with "error_number" (from the error catalog)
SHOW_ERROR_IF_STATIC(cond, error_number, error_string): if "cond" is TRUE, prints "ERROR: " followed by the error message associated with "error_number" (from the error catalog), and appends "error_string"

The static WARNING macros are:

SHOW_WARN_STATIC(error_number)                        : prints "WARNING: " followed by the error message associated with "error_number" (from the error catalog)
SHOW_WARN_STATIC(error_number, error_string)          : prints "WARNING: " followed by the error message associated with "error_number" (from the error catalog), and appends "error_string"
SHOW_WARN_IF_STATIC(cond, error_number)               : if "cond" is TRUE, prints "WARNING: " followed by the error message associated with "error_number" (from the error catalog)
SHOW_WARN_IF_STATIC(cond, error_number, error_string) : if "cond" is TRUE, prints "WARNING: " followed by the error message associated with "error_number" (from the error catalog), and appends "error_string"

Macro output

Error and warning messages printed via the SHOW_ERROR(_IF) and SHOW_WARN(_IF) macros will always contain:

The object id of the calling object.
The object type of the calling object.
The stellar type of the calling object (will be ”NONE” if the calling object is not a star-type object).
The function name of the calling function.

Any object that uses the the SHOW_ERROR(_IF) and SHOW_WARN(_IF) macros must expose the following functions:

OBJECT_ID ObjectId()** const { return m ObjectId; }
OBJECT_TYPE ObjectType()** const { return m ObjectType; }
STELLAR_TYPE StellarType()** const { return m StellarType; }

These functions are called by the ERROR and WARNING macros. If any of the functions are not applicable to the object, then they must return 'type::NONE' (all objects should implement ObjectId() correctly).

Error and warning messages displayed using the SHOW_ERROR(_IF)_STATIC and SHOW_WARN(_IF)_STATIC macros will always contain:

The function name of the calling function

but will not contain:

The object id of the calling object (not available in static functions)
The object type of the calling object (doesn't add enough information on its own)
The stellar type of the calling object (not available in static functions)

Handling errors at runtime

Early versions of COMPAS (versions prior to v03.00.00) did not have a coherent, robust error-handling strategy. In those versions, errors were typically displayed as either errors or warnings (depending on the severity) as they occurred, and evolution of the star (SSE mode) or binary (BSE mode) continued - users were expected to check errors or warnings displayed and use results with appropriate caution. This was not ideal.

In COMPAS version 03.00.00 the error handling philosophy was changed, and more coherent and robust error-handling code was implemented. The new error-handling philosophy is to stop evolution of a star or binary if an error occurs, and record in the (SSE/BSE) system parameters file the fact that an error occurred, and an error number identifying the error that occurred. This way users can check the system paramers file at the completion of a run for the disposition of a star or binary and, if the evolution of that star or binary was stopped because an error occurred, the actual error that occurred. Possible dispositions (for both stars and binaries) are given in the EVOLUTION_STATUS enum class and associate label map in typedefs.h.

The error-handling code implemented in v03.00.00 allows developers to terminate evolution of a star or binary if they determine that a condition encountered is sufficiently severe that allowing the evolution of the star or binary to continue would produce inconsistent or untrustable results. In those cases, the developers should terminate the evolution of the star or binary via the use of the THROW_ERROR* macros (defined in ErrorsMacros.h and described below).

Developers should use the SHOW_WARN* macros (defined in ErrorsMacros.h and described above) to alert users to conditions they want to bring to the attention of users, but are not sufficiently severe to warrant termination of the evolution of the star or binary.

The SHOW_ERROR* macros (defined in ErrorsMacros.h and described above) should be used sparingly - generally only in catch blocks for errors thrown, or in the (very few) sections of the code not covered by catch blocks.

The class member variable m_Error (in the BaseStar class for SSE; BaseBinaryStar for BSE) should not be set explicitly throughout the code - it is set in the appropriate error exception catch blocks. m_Error is the error value written to the log files. Note that it is possible that if users choose to add the STAR_PROPERTY::ERROR (SSE) or BINARY_PROPERTY::ERROR (BSE) to log files via the logfile-definitions file, the value of the error logged to those files may be 0 (ERROR::NONE) even for stars or binaries that ere eventually terminated due to an error - the error value is only set when the error occurs (and is thrown), so some records in some log files may already have been written prior to the error being identified and evolution terminated.

Floating-point errors in C++

In C++ implementations that implement the IEEE floating-point standard, in ordinary operation, the division of a finite non-zero floating-point value by 0[.0] is well-defined and results in +infinity if the value is greater than zero, -infinity if the value is less than zero, and NaN if the value is equal to 0.0, and in each case program execution continues uninterrupted. Integer division by 0 is undefined and results in a floating-point exception and the process is halted.

The GNU C++ implementation allows us to trap the following floating-point errors:

DIVBYZERO : division by zero, or some other asymptotically infinite result (from finite arguments).
INEXACT   : a value cannot be represented with exact accuracy (e.g. 0.1, 1.0/3.0, and sqrt(2.0)).
INVALID   : at least one of the arguments to a floating-point library function is a value for which the function is not defined (e.g. sqrt(-1.0))
OVERFLOW  : the result of an operation is too large in magnitude to be represented as a value of the return type.
UNDERFLOW : the result is too small in magnitude to be represented as a value of the return type.

When an enabled floating-point trap is encountered, a SIGFPE signal is raised. If we don't have a signal handler installed for SIGFPE the program is terminated with a floating-point exception. If we do have a signal handler installed for SIGFPE, that signal handler is invoked. Ordinarily, once the SIGFPE signal handler is invoked, there is no going back - after doing whetever we need to do to manage the signal, the only valid operations are to exit the program, or to longjmp to a specific location in the code. Fortunately the GNU C++ designers have given us another option: if we compile with the -fnon-call-exceptions compiler flag we can raise an exception safely from the SIGFPE signal handler, because the throw is just a non-local transfer of control (just like a longjmp), and then we can just catch the exception raised.

Floating-point errors in COMPAS

Instrumentation has been implemented in COMPAS v03.00.00 that traps DIVBYZERO, INVALID, OVERFLOW, and UNDERFLOW. Trapping INEXACT would mean we'd trap on just about every floating-point calculation (it is really just informational - we know there are many values we can't represent exactly in base-2).

We have 3 modes for the floating-point error instrumentation:

Instrumentation not active  : This mode is enabled with the option '--fp-error-mode OFF'  (This is the default mode).

                              This mode is just the default behaviour of the C++ compiler, as described above. In this mode,
                              the program execution will not be interrupted in the event of a floating-point error, but the
                              error reported in the system parameters file will be set to indicate if a floating-point error
                              occurred during evolution (and in this mode we can, and do, differentiate between 'DIVBYZERO',
                              'INVALID', 'OVERFLOW', and 'UNDERFLOW'). Note that an integer divide-by-zero will cause the
                              execution of the program to halt (and, rather obtusely, will report "Floating point exception").

Floating-point traps enabled: This mode is enabled with the option '--fp-error-mode ON'.

                              In this mode, floating-point traps 'DIVBYZERO', 'INVALID', 'OVERFLOW', and 'UNDERFLOW' are enabled.
                              When a floating-point operation traps, a 'SIGFPE' signal is raised and the 'SIGFPE' signal handler
                              is invoked, and the signal handler raises a 'runtime_error' exception, with a 'what' argument of
                              "FPE" (and in this mode we cannot, and do not, differentiate between 'DIVBYZERO', 'INVALID',
                              'OVERFLOW', and 'UNDERFLOW'). The exception raised will cause the execution of the program to halt
                              if it is not caught and managed. We catch 'runtime_error' exceptions in 'Star::Evolve()' for SSE
                              mode, in 'BaseBinaryStar::Evolve()' for BSE mode, and in 'main()' for errors that might occur
                              outside the evolution of stars or binaries.

Debug mode                  : This mode is enabled with the option '--fp-error-mode DEBUG'.

                              In this mode, floating-point traps 'DIVBYZERO', 'INVALID', 'OVERFLOW', and 'UNDERFLOW' are enabled.
                              When a floating-point operation traps, a 'SIGFPE' signal is raised and the 'SIGFPE' signal handler
                              is called, but instead of raising a 'runtime_error' exception, the signal handler prints the stack
                              trace that led to the error and halts execution of the program.  In this way, the user can determine
                              where (to the function level - we do not determine line numbers) the floating-point error occurred.

                              The construction of the stack trace in debug mode happens inside the signal handler, and the functions
                              used to do that are generally not signal safe - but we call 'std::exit()' anyway, so that should not
                              be a problem.

Throwing errors

Another additional set of macros is provided, for both static and non-static functions, that will, after displaying an error (as described above), throw an exception and cause the ordinary program flow to be interrupted. These are:

THROW_ERROR(error_number)                              : displays the error (as described above), then throws exception
THROW_ERROR(error_number, error_string)                : displays the error (as described above), then throws exception
THROW_ERROR_IF(cond, error_number)                     : if "cond" is TRUE, displays the error (as described above), then throws exception
THROW_ERROR_IF(cond, error_number, error_string)       : if "cond" is TRUE, displays the error (as described above), then throws exception

THROW_ERROR_STATIC(error_number)                       : displays the error (as described above), then throws exception
THROW_ERROR_STATIC(error_number, error_string)         : displays the error (as described above), then throws exception
THROW_ERROR_IF_STATIC(cond, error_number)              : if "cond" is TRUE, displays the error (as described above), then throws exception
THROW_ERROR_IF_STATIC(cond, error_number, error_string): if "cond" is TRUE, displays the error (as described above), then throws exception

In each case, the exception thrown by the THROW* macros is the 'error_number' argument cast as an integer, so it can be caught by using catch (int e) and inspecting "e".

Writing errors to file

When the user sets the --errors-to-file program option to TRUE (FALSE by default), errors and, if the user also sets the --enable-warnings program option to TRUE (FALSE by default), warnings will be written to a log file in addition to being displayed on stdout/stderr as they occur. In this case, the Log::Start() parameter p_ErrorsToFile will be TRUE, instructing the LOGGING service to write errors and warnings to a log file.

The filename to which error records are written when is declared in LogTypedefs.h – see the enum class LOGFILE and associated descriptor map LOGFILE_DESCRIPTOR. Currently the name is Error_Log.