Programming Reference:Error Handling

From BCI2000 Wiki
Jump to: navigation, search

Handling Errors

Types of Errors

We assume that all errors we need to consider fall into one of the following categories, each of which implies a different type of approach to error avoidance/error handling:

  • Parameter Setup Errors
  • Runtime Errors
  • Logic (Programming) Errors


Parameter Setup Errors

Definition of the Term

This category covers anything a user can do wrong by using a program with parameters that are out of range, inconsistent, or otherwise erroneous (e.g., by specifying an output file at a location where the user has no write permission).

Parameter setup errors, when unhandled, become runtime errors.

Strategies

As a guideline for approaching Parameter Setup Errors we adopt the following principle: "Whatever a user does from within an application program should never make that application crash."

In BCI2000, this translates into a thorough parameter check done by each module before any parameter settings are actually applied to the system.

Parameter checking should comprise

  • Range and Consistency checks, whereby ranges generally depend on the values of other parameters;
  • Signal property checks: Does the output signal of one filter meet the next filter's requirements for its input signal?
  • Resource availability checks:
    • Are needed system resources available? (E.g., is it possible to open a required sound output device?)
    • Are auxiliary files (e.g., media files) available and readable?
    • Do output files have legal file names? Are output files writeable? (We could even check whether there is enough space left to write the EEG file, but this is not practical because a concurrent process might use up the space while our system runs.)


In each of these cases, the user should get appropriate feedback guiding her towards fixing the problem.

Whenever the system tries to fix a parameter setup error by using some default set of parameters, it should do so only if

  • it presents the user with a warning that tells her what it did and why it did so, and if
  • the automatically fixed parameters are treated as if changed by the user, i.e. with a parameter check performed on them.

Otherwise, people may unknowingly using a system that doesn't do what they want it to, or have a system that creates new parameter inconsistencies when trying to fix others.

User Interface Details

The user interface for Parameter Setup Error handling is, along with the parameter setup dialog, part of the operator module. A first implementation of a GUI based user interface consists in a floating, non-modal error window popping up from the operator module that presents a list of error related textual messages to the user, allowing for browsing error messages while changing respective parameters via the parameter setup dialog. After the next parameter check, the operator module will close that window or replace its contents based on the result of the check. Parameter checking occurs when the user clicks the "SetConfig" button in the operator main window, followed by actually applying parameters if the check was successful.


Runtime Errors

Definition of the Term

This category covers everything that can go wrong in the course of running an application program, insofar as that malfunction is due to a lack of resources in the underlying system required for proper operation (i.e., not due to a programming error). Assuming that parameter checking has been implemented properly as outlined above, we can narrow the term `Runtime Error' to cases for which the following statement holds: A runtime error occurs whenever the system runs out of resources that were still available during parameter checking.

Typical reasons for this kind of error are

  • the system runs out of disk space while recording data,
  • files being moved, trashed, or locked by a concurrent process,
  • a network connection becomes unavailable.

Runtime errors, when unhandled, become logic errors because the code implies assumptions that no longer hold once a runtime error has occurred.

Strategies

In a properly designed and implemented system, runtime errors, in the restricted sense described above, will not occur frequently. However, as they are caused by undesired circumstances outside the scope of the application program itself, it seems important to provide information to the user that is as detailed as possible in order to enable her to prevent this type of situation in the future, and to make her aware of the fact that the application program depends on her willingness to provide a smooth operating environment. After displaying a message of this kind, it seems appropriate to simply abort execution altogether, while trying to avoid a loss of the data acquired up to that time.

User Interface Details

In general, it is desirable to have runtime errors displayed along with the user interface of the operator module. However, as this requires a working connection between the module where the error occurs, and the operator module, this may not always be possible. Therefore, in addition to an operator-based error reporting interface, each module should have a less demanding mechanism to provide error information to the user, e.g., a local log file.


Logic Errors

Definition of the Term

Logic, or programming, errors in general can be due to a programmer who, in his or her code, implicitly or explicitly makes assumptions that do not always hold.

Strategies

Programming errors are not supposed to occur at all in a tested version of an application. Therefore, instead of trying to 'handle' them, it is important to make them show up as close to their point of origin in the code as possible, by frequently and explicitly checking whether implicit assumptions actually hold, and aborting execution with an error message if this is not the case.

Typically, such checks use an "assertion" facility provided by the programming language. BCI2000 provides its own bciassert macro in BCIAssert.h to make sure that failed assertions result in messages that are displayed by the BCI2000 operator module. Unlike the standard assert macro, BCI2000 assertions do not evaluate to empty in release builds.

Aside from that, writing code as explicit, general, and simple as possible greatly reduces the possibility of making logic errors in the first place.

User Interface Details

As programming errors are errors about which a user can do nothing, simply aborting the program or module with an error message seems appropriate.


Implementation Details

Interface to the Programmer

Reporting Errors

For a simple and general way to provide user communication and a means of error reporting to a module's programmer, there exist two global objects derived from std::ostream, one named bciout and the other named bcierr, analogous to std::cout and std::cerr, where bciout is used to transfer general messages and warnings while bcierr takes actual errors. A code example then looks like this:

 #include "BCIError.h"
 ...
 using namespace std;
 ...
 ofstream outputStream( fileName );
 if( !outputStream.is_open() )
 {
   bcierr << "Cannot open the file \""
          << fileName
          << "\" for output"
          << endl;
 }

Furthermore, for handling runtime errors from which it is difficult to recover, code may throw an exception that will abort execution and eventually lead to an error message being sent to the operator module (for framework related details see the section entitled "Implementation on the Framework Side"):

#include "BCIException.h"
...
if( ernie.find( bert ) != ernie.end() )
   throw bciexception( "Ernie just ate Bert. I don't know how to tell the story." );
tellMyStory( ernie, bert );
...

Checking Parameters

Checking parameters is done in a separate member function of the filter base class which, similar to the member function that does the actual processing, takes input and output signal representatives as parameters, thus allowing for signal property checking.

For the actual implementation, its declaration is as follows:

void GenericFilter::Preflight ( const SignalProperties& Input, 
                                      SignalProperties& Output ) const;

For a filter class derived from GenericFilter, this function is supposed to perform parameter checking as described in the "Strategies" section. Instead of returning an error value, it writes possible error messages into bcierr. Furthermore, it communicates dimensions of its output signal which it guarantees not to exceed, and it does so by adjusting the properties of the second SignalProperties object in its argument list, e.g.

Output = SignalProperties( Input.Channels(), 1 );

or

Output = SignalProperties( 0, 0 );

if it declares not to use its output signal.

The const declaration for its this pointer prohibits initialization functionality from GenericFilter::Initialize() entering into Preflight(). Such behavior is unwanted because it would corrupt the idea of performing a complete parameter check before actually altering the state of a filter object.

A necessary condition for a correct implementation of the Preflight() function is that any parameter, as well as any state that will be accessed during the processing phase, be accessed from Preflight() at least once. For parameters and states defined by the filter itself (i.e. inside its constructor), range and accessibility checks are automatically performed by the framework; parameters and states defined by other filters must be explicitly accessed from Preflight(). If a GenericFilter descendant fails to access an externally defined parameter or state during Preflight(), the first access during the processing phase will result in a runtime error.

Accessing Environment Objects

We consider parameters and states part of the BCI2000 "environment". GenericFilter descendants have access to that environment, analogous to the concept of environment variables found in some operating systems. Internally, access to the environment is mediated through a mix-in-class named Environment that provides accessor symbols to a filter programmer.

Low Level Access to Environment Objects is provided by the following symbols:

  • Parameters syntactically behaves like a ParamList*,
  • States behaves like a StateList*,
  • and Statevector behaves like a StateVector*.

As an example,

float  myParameterValue = 0.0;
Param* param = Parameters->GetParamPtr( "MyParameter" );
if( param )
  myParameterValue = atof( param->GetValue() );
else
  bcierr << "Could not access \"MyParameter\"" << endl;

Unlike true pointers, these symbols cannot be assigned any values, cannot be assigned to variables, or have other manipulating operators applied. For example, the lines

delete Parameters;
Parameters = new ParamList;

will all result in compiler errors.

Convenient Access to Environment Objects is possible through a number of symbols which offer built-in checking and error reporting:

  • Parameter(Name)[(Index 1[, Index 2])

This symbol stands for the value of the named parameter. Indices may be given in numerical or textual form; if omitted, they default to 0. The type of the symbol Parameter() may be numerical or a string type, depending on its use. (If the compiler complains about ambiguities, use explicit typecasts.) If a parameter with the given name does not exist, an error message is written into bcierr. If the specified indices do not exist, no error is reported. In both cases, on read access, the string constant "0" resp. the number 0 is returned.

Examples:

int myValue = Parameter( "MyParam" ); 
string myOtherValue = Parameter( "MyOtherParam" ); 
Parameter( "My3rdParam" )( 2, 3 ) = my3rdValue; 
  • OptionalParameter(Name[, Default Value])(Index 1[, Index 2])

This symbol behaves like the symbol Parameter() but will not report an error if the parameter does not exist. Instead, it will return the default value given in its first argument. Assignments to this symbol are not possible.

  • State(Name)

This symbol allows for reading a state's value from the state vector and setting a state's value in the state vector. Trying to access a state that is not accessible will result in an error reported via bcierr.

Examples:

short currentStateOfAffairs = State( "OfAffairs" ); 
State( "OfAffairs" ) = nextStateOfAffairs;
  • OptionalState(Name[, Default Value])

Analagous to OptionalParameter(), this symbol does not report an error if the specified state does not exist but returns the given default value. Assignments to this symbol are not possible.

  • PreflightCondition(Condition)

This symbol is meant to be used inside implementations of GenericFilter::Preflight(). If the boolean condition given as its argument is false, it will output an error message into bcierr containing the condition given in its argument.

Example:

PreflightCondition( Parameter( "TransmitCh" ) <= Parameter( "SourceCh" ) );

If TransmitCh is greater than SourceCh, a message will be sent to bcierr and displayed to the user, stating:

A necessary condition is violated. Please make sure that the following is true: Parameter( "TransmitCh" ) <= Parameter( "SourceCh" )

Implementation on the Framework Side

The behaviour of the operator module in response to an error message that arrives from one of the modules depends on its context, i.e., on the execution phase the system is in. That way, no additional programming interface elements visible to a filter/module programmer are needed to implement an error handling scheme as described in the "Handling Errors"section.

During the preflight phase, errors are Configuration Errors. A module's framework code behind bcierr just collects error messages; on return from the preflight function, it sends those messages to the operator module which then, from the contents of the message (i.e., whether it was empty or not), determines whether the preflight was successful; on not receiving any message after some timeout it assumes a broken connection or a crashed module.

(For now, a simple timeout scheme with a fixed timeout interval of 5s seems appropriate. In the future, one might consider a module requesting additional timeout periods if it expects lengthy calculations.)

During all other phases, the code behind bcierr immediately (i.e., on flushing the std::ostream) sends its message buffer to a log file as well as to the operator module, indicating a Runtime Error to the operator module which will, in turn, halt the system,shut down the other modules, and display the message to the user.

In addition, the top level exception handling code of each module contains similar functionality, sending an exception's associated description string into a log file and to the operator module, if possible, then quitting the module in which the exception occurred. This not only ensures a proper general handling of exceptions within the framework but also allows a programmer to handle Runtime Errors by raising her own exceptions, eliminating the need to take care of the error condition in the code following the detection of an error.


See also

Programming Reference:Errors and Warnings, Programming Reference:Environment Class, Programming Reference:Debug Output