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.
#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.
#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 |
#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.
#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;
}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/longjmpfor rare, low-level scenarios (e.g. signal handlers); avoid it for everyday error handling.