Skip to content

Error handling

oguyon edited this page May 24, 2020 · 9 revisions

This page will be moved to source code documentation when finalized

Inspired from these recommendations, and this presentation.

Background

As a general practice, functions should success or error code when possible, but for small functions (example: computing a cosine), it is more convenient to return the result of the computation.

In-band error indicators are bad practice (see links above) and should be avoided whenever possible. One issue with this option is that is makes it impossible to adopt a standard error code shared by all/most function, as a "useful" value is a per-function concept.

Using EXIT_SUCCESS and EXIT_FAILURE for anything else than calls to the exit() function or the return from main() is bad practice, and does not allow for fine-grained error checking.

Proposed guidelines for developers are provided in this document.

1. Functions return value

Guideline

Whenever practical: function should return error code, and computation results should be returned by pointer

The function should return a value of type errno_t. See examples and guidelines here

errno_t myfunction(int *returnval);

Note that errno_t is part of C11's annex K, so it may not be supported by all compiles. Add the following code

#ifndef __STDC_LIB_EXT1__
typedef int errno_t;
#endif

Functions that return an error code would be clearly identified by the errno_t return type:

errno_t computecosine_returnscode(float inputval, float *returnval);   // returns error code
float   computecosine_returnserrc(float inputval);                     // returns computation result

Error codes

Error codes should be included in CLIcore.h, as follows:

#define RETURN_SUCCESS       0 
#define RETURN_FAILURE       1   // generic error code
#define RETURN_MISSINGFILE   2  

2. Handling errors

The PRINT_WARNING and PRINT_ERROR macros are provided in CLIcore.h

2.1. PRINT_ERROR

PRINT_ERROR();

PRINT_ERROR records the current state of the program (testpoint), which is displayed upon calling abort(). It is recommended to call abort() after PRINT_ERROR, as this macro is intended for error conditions that cannot be handled otherwise.

2.2. PRINT_WARNING

PRINT_WARNING();

Use PRINT_WARNING to issue warning and continue execution.

2.3. Examples

PRINT_WARNING("something is not quite right");

if(a<0)
PRINT_WARNING("a should be positive, but a = %f", a);

if(a<0) {
PRINT_ERROR("a has to be positive, but a = %f", a);
abort();
}

Note that PRINT_ERROR can be followed by abort() if the error cannot be handled otherwise.

3. Testing return value of standard functions

Always test return value of std functions

3.1. macros with error checking

The macros, defined in CLIcore.h, perform conservative error checking and will abort on error.:

EXECUTE_SYSTEM_COMMAND

# execute system command. Use instead of system()
# checks for command string buffer overflow
# checks for command return
int a = 4;
EXECUTE_SYSTEM_COMMAND("echo \"%d\"", a);

WRITE_IMAGENAME

# write image name to existing image string
char imtest[STRINGMAXLEN_IMGNAME];
int a = 426;
WRITE_IMAGENAME(imtest, "fr53im_%d", a);

WITE_FILENAME

# write fille name to existing string
char filetest[STRINGMAXLEN_FILENAME];
int a = 426;
WRITE_FILENAME(filetest, "fr53im_%d.txt", a);

WITE_FULLFILENAME

# write image name to existing string
char ffiletest[STRINGMAXLEN_FULLFILENAME];
int a = 426;
WRITE_FULLFILENAME(ffiletest, "/home/me/fr53im_%d.txt", a);

3.2. Custom examples

fscanf()

int fscanfcnt;

fscanfcnt = fscanf(fp, "%s", streamfname);
if(fscanfcnt == EOF) {
    if(ferror(fp)) {
        perror("fscanf");
    } else {
        fprintf(stderr, "Error: fscanf reached end of file, no matching characters, no matching failure\n");
    }
    return RETURN_FAILURE;
} else if(fscanfcnt != 2) {
    fprintf(stderr, "Error: fscanf successfully matched and assigned %i input items, 2 expected\n", fscanfcnt);
    return RETURN_FAILURE;
}

system()

if(system("ls") != 0) {
	PRINT_ERROR("system() returns non-zero value");
}

sprintf()

if(sprintf(name, "image1", loop)<1) {
    PRINT_ERROR("sprintf wrote <1 char");
}

snprintf()

Use snprintf instead of sprintf to avoid buffer overflow.

char namestring[MAXSTRLEN];

{ // code block write image name
    int slen = snprintf(imname, STRINGMAXLEN_IMGNAME, "somefilename");
    if(slen<1) {
	PRINT_ERROR("snprintf wrote <1 char");
	abort(); // can't handle this error any other way
    }
    if(slen >= STRINGMAXLEN_IMGNAME) {
	PRINT_ERROR("snprintf string truncation");
	abort(); // can't handle this error any other way
    }
} // end code block

malloc()

int msize=10;
farray = (float*) malloc(sizeof(float)*msize);
if(farray == NULL) {
	PRINT_ERROR("malloc returns NULL pointer, size %d", msize);
	abort(); // or handle error in other ways
}

fopen()

fp_test = fopen("testfile.log", "w");
if(fp_test == NULL) {
	PRINT_ERROR("Cannot open file testfile.log");
	abort();
}

4. Tracing errors in DEBUG mode

Some bugs can be found with the printf-based method: include printf statements to find where/why the code crashes. Use stderr instead of stdout: as the stdout is buffered, you application can crash before flushing the stdout buffer. For fast debug, it's safer to you stderr. Alternatively, use fflush(stdout).

Instead of using the printf method, we recommend compiling it with the NDEBUG flag.

General Practices:

  • Use assertions (assert()), which are enabled with the NDEBUG flag
  • Use DEBUG_TRACEPOINT macro which insert test points, also enabled by NDEBUG flag
  • When an error cannot be handled, call abort()

4.1. Compiling with debug options

cd _build
rm CMakeCache.txt
make clean
# add additional cmake options as needed
cmake -DCMAKE_BUILD_TYPE=Debug ..
sudo make install

The cmake Debug build type sets NDEBUG to enables debugging features, and uses compilation options "-Wall -g -O0".

4.1. Catching signals (enabled by default)

To enable signals catching, the following function is called:

errno_t set_signal_catching();  // code in CLIcore.c

This is enabled by default.vSignal catching will make use of test points (see below): upon exit, it will print the state of the code as last probed by the DEBUG_TRACEPOINT macro.

4.2. Using DEBUG_TRACEPOINT macro

Call the DEBUG_TRACEPOINT macro to collect and stores information at any point of the source code. The corresponding information will be included in the exit report written upon abnormal program exit.

The macro source code is in CLIcore.h :

#ifdef NDEBUG || defined(STANDALONE)
#define DEBUG_TRACEPOINT(...)
#else
#define DEBUG_TRACEPOINT(...) do { \
sprintf(data.testpoint_file, "%s", __FILE__); \
sprintf(data.testpoint_func, "%s", __func__); \
data.testpoint_line = __LINE__; \
clock_gettime(CLOCK_REALTIME, &data.testpoint_time); \
sprintf(data.testpoint_msg, __VA_ARGS__); \
} while(0)
#endif

To use testpoints, compile in DEBUG mode, and then call the DEBUG_TRACEPOINT macro. The macro should always be called before abort().

Examples :

DEBUG_TRACEPOINT("some informative comment");
assert(a>0);

DEBUG_TRACEPOINT(" ");  // no comment. Note that space char is needed.

if(a<b) {
    DEBUG_TRACEPOINT("a is %f, should be less than %f", a, b);
    abort();
}

Clone this wiki locally