Simplified error handling without goto

Before ‘goto’ became a no-no, a common way of error handling was a jump on an unexpected return of a function. Max Vilimpoc gives a good example here: https://vilimpoc.org/research/raii-in-c/. The pattern used is the same in this simplified version:

int main() {
    resource_t *res_1 = init_1();
    if ( ! res_1 ) goto cleanup_end;

    resource_t *res_2 = init_2();
    if ( ! res_2 ) goto cleanup_res_1;

    resource_t *res_3 = init_3( res_1, res_2 );
    if ( ! res_3 ) goto cleanup_res_2;

    // do something with the resources

cleanup_res_3:
    deinit_3( res_3 );
cleanup_res_2:
    deinit_2( res_2 );
cleanup_res_1:
    deinit_1( res_1 );
cleanup_end:
    return 0;
}

A sequence of nested if-statements for error handling is logically similar, but far less elegant. When the code becomes more complex it will become harder to keep track of the nesting levels.

int main() {
    resource_t *res_1;
    resource_t *res_2;
    resource_t *res_3;

    res_1 = init_1();
    if ( res_1 ) {
        res_2 = init_2( res_1 );
        if ( res_2 ) {
            res_3 = init_3( res_1, res_2 );

            // do something with the resources

            if ( res_3 ) {
                deinit_3( res_3 );
            }
            deinit_2( res_2 );
        }
        deinit_1( res_1 );
    }

    return 0;
}

Error handling without goto

It is not that hard to rewrite the ‘goto’ implementation without the use of that keyword if you really have to (e.g. due to standard compliance). The cost is some extra lines of code and a possible reduction in performance. The trick is to separate the sequence in separate states indicating the progress made (e.g. in terms of resources that need clean-up). These states can be modelled as an enumeration. Loop over the states and break in case of an error. The reverse loop (starting at the current state) can be used for clean-up actions.

int main() {
    resource_t *res_1;
    resource_t *res_2;
    resource_t *res_3;
    
    enum {
        state_begin = 0        ,
        state_res_1_initialised,
        state_res_2_initialised,
        state_res_3_initialised,
        state_last
    } state = state_begin;
    
    for ( ; state < state_last; ++state ) {
        if ( state == state_begin ) {
            res_1 = init_1();
            if ( ! res_1 ) {
                break;
            }
        }
        if ( state == state_res_1_initialised ) {
            res_2 = init_2( res_1 );
            if ( ! res_2 ) {
                break;
            }
        }
        if ( state == state_res_2_initialised ) {
            res_3 = init_3( res_1, res_2 );
            if ( ! res_3 ) {
                break;
            }
        }
        if ( state == state_res_3_initialised ) {
            // do something with the resources
        }
    }

    for ( ; state > 0; --state) {
        switch( state ) {
            case state_res_3_initialised :
                deinit_3(&res3);
                break;
            case state_res_2_initialised :
                deinit_2(&res2);
                break;
            case state_res_1_initialised :
                deinit_1(&res1);
                break;
             default:
                break;
        }
    }
    return 0;
}

A modified version of Max Vilimpoc’s code that follows this approach can be found here. I would have preferred to model the resource allocation part as a switch statement too, but this would have made it impossible to use ‘break’ to exit the loop. Note that the use of ‘break’ and ‘continue’ may also be against some coding standards, but these keywords can be easily avoided by an extra control variable in the for-loop. What did we gain with this exercise? We got rid of ‘goto’, which makes it easier to comply to some standards and we made the flow a bit more predictable by eliminating infinite loops by design. There are no backward jumps possible and all for loops are bounded. What did we lose? We need a lot more lines of code and the administration of the state and extra comparisons may come at some performance cost. Was it worth it? Probably only if you are (or need to be) pedantic about not using ‘goto’.

Simplified error handling

My preferred way of error handling to perform each step that needs error handling in a separate function with an inout error parameter. If on input the error value indicates success the function performs its regular job, otherwise it returns immediately without changing the error parameter. It is straightforward to wrap existing functions to conform to this interface.

resource_t *wrapped_init_1(error_t *pError) {
    if ( *pError != ERROR_NONE )
        return NULL;

    resource_t *ret = init_1();
    if ( ! ret ) {
        *pError = ERROR_RESOURCE_1_FAILED;
    }

    return ret;
}

This way the functions can simply be used in a sequence. Errors can easily be checked for at the end of the sequence. If you need to do (conditional) clean-up, it is easier to check whether the resource is allocated rather than checking the state of progress so far. For example, use null to initialise a pointer to memory. In the clean-up section you can just free the memory (posix free with null as parameter has no effect). It is uncommon to report errors on deallocating resources (you are already done with the work, just deallocate if possible). For robustness it is best to modify the (reference to a) resource such that it is easy to check whether the resource is still allocated. For example:

void wrapped_free(void **p) {
    free(*p);
    *p = NULL;
}

Depending on the situation you may still want to log or assert whenever an attempt is made to deallocate a resource which has not been allocated in the first place. As for error reporting you can simply pass (preferably as inout parameter) the error parameter.

int main() {
    error_t  error;

    resource_t *res_1 = init_1( &error );
    resource_t *res_2 = init_2( res_1, &error );
    resource_t *res_3 = init_3( res_1, res_2, &error );

    // do something with the resources

    deinit_3(&res3);
    deinit_2(&res2);
    deinit_1(&res1);

    return -error;
}

To show the feasibility of this approach you can find a modified version of Max Vilimpoc’s code with simplified error handling here. The number of code lines increases a bit due to the required wrappers for library functions. In real life situations you will find the lines of code will decrease as the wrappers can be reused and the main flow is much simpler!