CError Handling Strategies

Error Handling Strategies

Unlike languages with try/catch exceptions, C has no built-in exception mechanism. Error handling in C is a matter of discipline and convention: functions communicate failure through their return values (or output parameters), and it is entirely the caller's job to check for and react to those failures. Skipping that check is one of the most common sources of bugs and security vulnerabilities in C programs.

Idiom 1: Return Codes

The most common pattern: a function returns an integer where a special value (often 0, -1, or a named constant) signals failure, and the caller checks it immediately.

C
#include <stdio.h>

int divide(int a, int b, int *result) {
    if (b == 0) {
        return -1; /* failure: division by zero */
    }
    *result = a / b;
    return 0; /* success */
}

int main(void) {
    int result;

    if (divide(10, 0, &result) != 0) {
        fprintf(stderr, "Error: division by zero\n");
        return 1;
    }

    printf("Result: %d\n", result);
    return 0;
}
Idiom 2: Output Parameters for Error Detail

When a bare return code isn't descriptive enough, functions often take an extra pointer parameter used purely to report richer error information — an error code, a message buffer, or a bool flag — while the primary return value is reserved for the "real" result.

C
#include <stdio.h>

typedef enum {
    ERR_NONE = 0,
    ERR_DIVIDE_BY_ZERO,
    ERR_OVERFLOW
} ErrorCode;

int safeDivide(int a, int b, ErrorCode *error) {
    if (b == 0) {
        *error = ERR_DIVIDE_BY_ZERO;
        return 0;
    }
    *error = ERR_NONE;
    return a / b;
}

int main(void) {
    ErrorCode error;
    int result = safeDivide(10, 0, &error);

    if (error != ERR_NONE) {
        fprintf(stderr, "Divide failed with error code %d\n", error);
        return 1;
    }

    printf("Result: %d\n", result);
    return 0;
}
The Golden Rule: Check Every Fallible Call, Immediately

Many standard library functions can fail, and C will not stop you from ignoring that. The discipline that separates robust C code from fragile C code is checking the return value right after the call, before using any data the call was supposed to produce.

Function

Failure signal

What to do

malloc / calloc / realloc

Returns NULL

Check before dereferencing the pointer

fopen

Returns NULL

Check before reading/writing the file

fgetc / getchar

Returns EOF

Stop reading; distinguish EOF from a real error with feof/ferror

scanf / fscanf

Returns fewer items than expected (or EOF)

Verify the count matches expected fields

printf / fprintf

Returns a negative value

Rare in practice, but technically indicates an output error

fread / fwrite

Returns fewer elements than requested

Check the count against what you asked for

C
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *numbers = malloc(10 * sizeof(int));
    if (numbers == NULL) {
        fprintf(stderr, "Allocation failed\n");
        return 1;
    }

    FILE *file = fopen("data.txt", "r");
    if (file == NULL) {
        fprintf(stderr, "Could not open data.txt\n");
        free(numbers);
        return 1;
    }

    /* ... use numbers and file safely here ... */

    fclose(file);
    free(numbers);
    return 0;
}
setjmp/longjmp: Non-Local Jumps

The <setjmp.h> header provides setjmp() and longjmp(), which let a program save an execution point and later jump back to it from deep within nested function calls — a crude, C-flavored substitute for exceptions.

C
#include <stdio.h>
#include <setjmp.h>

jmp_buf recoveryPoint;

void riskyOperation(int fail) {
    if (fail) {
        longjmp(recoveryPoint, 1); /* jump back to setjmp's call site */
    }
    printf("riskyOperation succeeded\n");
}

int main(void) {
    if (setjmp(recoveryPoint) == 0) {
        riskyOperation(1);
        printf("This line never runs after a longjmp\n");
    } else {
        printf("Recovered from a failure via longjmp\n");
    }
    return 0;
}
Note
`setjmp`/`longjmp` are mentioned here for completeness, not as a recommendation. They are generally **discouraged** in modern C: they bypass normal stack unwinding, don't run cleanup code (no destructors, obviously, but also no `free()` calls you might expect to happen along the way), and make control flow much harder to follow. Prefer explicit return codes for the vast majority of error handling.
Summary of Idioms
  • Return a status/error code and use output parameters for the "real" result.

  • Return a valid value or a sentinel like NULL/-1, with the actual error detail elsewhere (e.g. errno).

  • Check every fallible call's return value before trusting its output.

  • Reserve setjmp/longjmp for rare, low-level scenarios (e.g. signal handlers); avoid it for everyday error handling.

Tip
Treat "I forgot to check a return value" as a bug class of its own — many historical security vulnerabilities trace back to exactly this: an unchecked `malloc` result being dereferenced, or an unchecked `fopen` failure leading to reads from an invalid file handle.