Skip to content
Home » Software Design » Error Handling Patterns in C — With Practical Examples

Error Handling Patterns in C — With Practical Examples

Error Handling Patterns featured image with blue-purple background, Practical badge, Er icon, in C 5 Approaches subtitle in the Software Design in C series by NerdyElectronics
Software Design Principles in C
Part 20 of 31View Full Path →

KEY TAKEAWAYS

  • Always check return codes — unchecked errors are the #1 source of C bugs
  • Use the goto cleanup pattern for functions that acquire multiple resources
  • Use error callbacks to decouple error reporting from error handling
  • Choose the pattern that fits your context — simple for small functions, richer for complex systems
  • Be consistent within a project — pick one or two patterns and use them everywhere

Why Error Handling in C Is Hard

C has no exceptions, no try/catch, and no automatic cleanup. When something fails, you must detect it, report it, and clean up resources manually. Getting this right is one of the most important skills in C programming — especially in embedded systems where unhandled errors can cause hardware damage or safety hazards.This post covers the most common error handling patterns in C, with their trade-offs.

Pattern 1: Return Codes

The simplest and most common pattern. Functions return a status code indicating success or the type of failure.
typedef enum {
    STATUS_OK = 0,
    STATUS_ERR_NULL,
    STATUS_ERR_RANGE,
    STATUS_ERR_TIMEOUT,
    STATUS_ERR_HARDWARE,
} Status;

Status sensor_init(Sensor *s, int channel) {
    if (s == NULL)                    return STATUS_ERR_NULL;
    if (channel  15) return STATUS_ERR_RANGE;

    s->channel = channel;
    if (!adc_configure(channel)) {
        return STATUS_ERR_HARDWARE;
    }
    s->initialized = true;
    return STATUS_OK;
}

// Caller checks the return code
Status st = sensor_init(&my_sensor, 3);
if (st != STATUS_OK) {
    log_error("Sensor init failed: %d", st);
    return st;  // Propagate the error
}
Pros: Simple, no overhead, widely understood. Cons: Easy to ignore the return value. Output values must go through pointers.

Pattern 2: Output Parameters + Return Code

When a function needs to return both a result and a status, use the return value for the status and an output pointer for the data.
Status sensor_read(Sensor *s, float *out_value) {
    if (s == NULL || out_value == NULL) return STATUS_ERR_NULL;
    if (!s->initialized)                return STATUS_ERR_HARDWARE;

    int raw = adc_read(s->channel);
    if (raw < 0) return STATUS_ERR_HARDWARE;

    *out_value = (float)raw * 0.01f;
    return STATUS_OK;
}

// Usage
float temperature;
if (sensor_read(&temp_sensor, &temperature) == STATUS_OK) {
    display_temperature(temperature);
}

Pattern 3: The Goto Cleanup Pattern

When a function acquires multiple resources (memory, file handles, hardware locks), cleanup on error becomes complex. The goto cleanup pattern is the standard C solution — and it is considered good practice, not bad style.
Status process_file(const char *path) {
    Status result = STATUS_OK;
    FILE *f = NULL;
    uint8_t *buffer = NULL;

    f = fopen(path, "rb");
    if (f == NULL) {
        result = STATUS_ERR_HARDWARE;
        goto cleanup;
    }

    buffer = malloc(1024);
    if (buffer == NULL) {
        result = STATUS_ERR_NULL;
        goto cleanup;
    }

    size_t n = fread(buffer, 1, 1024, f);
    if (n == 0) {
        result = STATUS_ERR_HARDWARE;
        goto cleanup;
    }

    // Process data...
    result = parse_data(buffer, n);

cleanup:
    free(buffer);    // free(NULL) is safe
    if (f) fclose(f);
    return result;
}
Why goto? Without it, you end up with deeply nested if-else blocks or duplicated cleanup code. The goto cleanup pattern is used extensively in the Linux kernel and most professional C codebases.

Pattern 4: Error Callback

For libraries and reusable modules, let the caller decide how to handle errors by registering a callback.
typedef void (*ErrorHandler)(int code, const char *msg);

static ErrorHandler g_error_handler = NULL;

void set_error_handler(ErrorHandler handler) {
    g_error_handler = handler;
}

static void report_error(int code, const char *msg) {
    if (g_error_handler) {
        g_error_handler(code, msg);
    }
}

// Inside library code
void comm_send(const uint8_t *data, int len) {
    if (uart_send(data, len) < 0) {
        report_error(ERR_COMM, "UART send failed");
    }
}

// Application registers its handler
void my_error_handler(int code, const char *msg) {
    printf("[ERR %d] %s\n", code, msg);
    led_set(LED_RED, ON);
}

set_error_handler(my_error_handler);

Pattern 5: Error Struct (Rich Error Info)

typedef struct {
    int   code;
    char  message[64];
    const char *file;
    int   line;
} Error;

#define MAKE_ERROR(err, c, msg) do { 
    (err)->code = (c);             
    strncpy((err)->message, (msg), sizeof((err)->message) - 1); 
    (err)->file = __FILE__;        
    (err)->line = __LINE__;        
} while(0)

Status sensor_init(Sensor *s, int ch, Error *err) {
    if (s == NULL) {
        MAKE_ERROR(err, ERR_NULL, "Sensor pointer is NULL");
        return STATUS_ERR_NULL;
    }
    // ...
    return STATUS_OK;
}
This gives you detailed error context — which file, which line, what message. Very useful for debugging embedded systems where you cannot always run a debugger.

Comparison

PatternBest ForOverhead
Return codesSimple functionsMinimal
Output params + returnFunctions that produce a valueMinimal
Goto cleanupFunctions with multiple resourcesNone
Error callbackLibraries, reusable modulesLow
Error structDebug-rich systemsModerate

Key Takeaways

  • Always check return codes — unchecked errors are the #1 source of C bugs
  • Use the goto cleanup pattern for functions that acquire multiple resources
  • Use error callbacks to decouple error reporting from error handling
  • Choose the pattern that fits your context — simple for small functions, richer for complex systems
  • Be consistent within a project — pick one or two patterns and use them everywhere

Leave a Reply

Your email address will not be published. Required fields are marked *