I'm starting a new, small C project and would like to have the safest, cleanest and shortest error handling code possible.Here is a comparison of different ways to handle errors in the same C function.Which one should I use and why ?The function, called db_create, creates a new ImageDatabase instance.Table Of ContentSnippet 1 uses nested ifSnippet 2 uses nested elseSnippet 3 uses gotoSnippet 4 improves on snippet 3 with two macrosSnippet 1This version (30 lines of code) uses nested if to achieve error handling without duplication. The code uses the following structure multiple times:if (allocate resource and check if success) { // push on the (imaginary) stack  <further code using the resource>  free resource // pop the (imaginary) stack} else {  signal error // unable to push further}Upside: the nesting provides a sort of stack unwinding that ensures correct resources releasingDownside: [locality cost] sometimes the else is far from the condition and this makes code hard to track down, see also arrow anti-pattern.ImageDatabase*db_create_if(char const* filename,             uint32_t max_images,             struct resolution resolutions[NB_RESOLUTIONS - 1]){    ImageDatabase* db = allocate_empty_db();    if (db) {        memcpy(db->resolutions, resolutions, (NB_RESOLUTIONS - 1) * sizeof(struct resolution));        db->max_nb_images = max_images;        if ((db->file = fopen(filename, r+b))) {            if ((db->metadatas = calloc(max_images, sizeof(struct metadata)))) {                if (fwrite(db, 1, sizeof(ImageDatabase), db->file) == 1) {                    return db;                    //                    //error handling below:                } else {                    fprintf(stderr, unable to write newly created database to %s, filename);                }                free(db->metadatas);            } else {                fprintf(stderr, unable to allocate metadatas);            }            fclose(db->file);        } else {            fprintf(stderr, unable to open %s, filename);        }        free(db);    } else {        fprintf(stderr, unable to allocate database);    }    return NULL;}Snippet 2This version (30 lines of code) uses nested else to achieve error handling without duplication.The code uses the following structure multiple times: if (allocate resource and check if error) {     signal error } else {     <further code using the resource>     free resource }Upsides: the else clause is short so there is no problem with code locality      the nesting provides a sort of stack unwinding that ensures correct resources releasingDownsides: error handling code mixed with logic, lots of nesting, hard to readImageDatabase*db_create_else(char const* filename,           uint32_t max_images,           struct resolution resolutions[NB_RESOLUTIONS - 1]){    ImageDatabase* db = allocate_empty_db();    if (db == NULL) {        fprintf(stderr, unable to allocate database);    } else {        memcpy(db->resolutions, resolutions, (NB_RESOLUTIONS - 1) * sizeof(struct resolution));        db->max_nb_images = max_images;        if ((db->file = fopen(filename, r+b)) == NULL) {            fprintf(stderr, unable to open %s, filename);        } else {            if ((db->metadatas = calloc(max_images, sizeof(struct metadata))) == NULL) {                fprintf(stderr, unable to allocate metadatas);            } else {                if (fwrite(db, 1, sizeof(ImageDatabase), db->file) != 1) {                    fprintf(stderr, unable to write newly created database to %s, filename);                } else {                    return db;                    //cleanup code below :                }                free(db->metadatas);            }            fclose(db->file);        }        free(db);    }    return NULL;}Snippet 3This version (34 lines of code) uses goto to achieve error handling without duplication and without deep nesting. The code has the following structure:if (allocate resource and check for error) { // push on the (imaginary) stack     <signal error>     goto pop_label; } <further code using the resource> pop_label: <cleanup code> // pop the (imaginary) stackUpsides: the stack unwinding pattern with goto is common in C and ensures correct resources releasing, link1, link2.Downside: some error handling code mixed with logicImageDatabase*db_create_goto(char const* filename,           uint32_t max_images,           struct resolution resolutions[NB_RESOLUTIONS - 1]){    ImageDatabase* db = allocate_empty_db();    if (db == NULL) {        fprintf(stderr, unable to allocate database);        goto db_fail;    }    memcpy(db->resolutions, resolutions, (NB_RESOLUTIONS - 1) * sizeof(struct resolution));    db->max_nb_images = max_images;    if ((db->file = fopen(filename, r+b)) == NULL) {        fprintf(stderr, unable to open %s, filename);        goto file_fail;    }    if ((db->metadatas = calloc(max_images, sizeof(struct metadata))) == NULL) {        fprintf(stderr, unable to allocate metadatas);        goto metadata_fail;    }    if (fwrite(db, 1, sizeof(ImageDatabase), db->file) != 1) {        fprintf(stderr, unable to write newly created database to %s, filename);        goto write_fail;    }    return db;write_fail:      free(db->metadatas);metadata_fail:      fclose(db->file);file_fail:      free(db);db_fail:      return NULL;}Snippet 4This version (22 lines of code) uses goto to achieve error handling without duplication and without deep nesting and two macros to shorten the code and to avoid mixing the logic with the error handling. The code has the following structure:try( allocate resource and check for error, label_for_error); // push on the (imaginary) stack <further code using the resource> LABEL(label_for_error, code_to_execute_once_if_try_failed); // pop the (imaginary) stackUpside: the shortest version and doesn't mix error signaling/handling with logicDownside: uses two (short) macrosNote: the team is very small (max 2/3 persons, probably less) and, if used, these macros will appear in almost every function so I expect that we will get used to it very quickly.The macros:#define try( stmt, label )           \    do{                              \        if(!(stmt)){ goto label; } } \    while(false)                    #define LABEL( label, execute_once_code )        \    do{                                          \        if(false){ label: execute_once_code; } } \    while(false)The code:ImageDatabase*db_create_try(char const* filename,           uint32_t max_images,           struct resolution resolutions[NB_RESOLUTIONS - 1]){    //logic is not mixed with error handling:    ImageDatabase* db;    try( db = allocate_empty_db(), db_fail);    memcpy(db->resolutions, resolutions, (NB_RESOLUTIONS - 1) * sizeof(struct resolution));    db->max_nb_images = max_images;    try( db->file = fopen(filename, r+b), file_fail);    try( db->metadatas = calloc(max_images, sizeof(struct metadata)), metadata_fail);    if (fwrite(db, 1, sizeof(ImageDatabase), db->file) != 1) { goto fwrite_fail; }    return db;    //error handling:    LABEL( fwrite_fail, fprintf(stderr, unable to write newly created database to %s, filename));    free(db->metadatas);    LABEL( metadata_fail, fprintf(stderr, unable to allocate metadatas));    fclose(db->file);    LABEL( file_fail, fprintf(stderr, unable to open %s, filename));    free(db);    LABEL( db_fail, fprintf(stderr, unable to allocate database));    return NULL;}
Four versions of a database-creation function with different error handling
c;file;error handling;comparative review;macros