Error Handling in C
Learn errno, perror, strerror, setjmp/longjmp, and defensive programming patterns for robust C programs.
Return Value Conventions
C has no exceptions. Error handling is explicit: functions signal failure through their return value, and callers must check it. This makes error paths visible in the code, which is both a strength (no hidden control flow) and a burden (you must remember to check). Consistency within a codebase matters — pick one convention and stick with it.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* Pattern 1: return NULL on error — natural for functions that allocate or find */
char *duplicate_upper(const char *s) {
if (!s) return NULL; /* validate input first */
char *result = malloc(strlen(s) + 1);
if (!result) return NULL; /* malloc failed — propagate NULL to caller */
for (int i = 0; s[i]; i++) {
result[i] = (char)toupper((unsigned char)s[i]);
}
result[strlen(s)] = '\0';
return result;
}
/* Pattern 2: return 0 on success, -1 on error — natural for operations */
int read_int_from_file(const char *filename, int *out) {
FILE *fp = fopen(filename, "r");
if (!fp) return -1;
int ok = fscanf(fp, "%d", out) == 1;
fclose(fp);
return ok ? 0 : -1;
}
/* Pattern 3: return an error code enum — best for complex APIs with many error types */
typedef enum {
ERR_OK = 0,
ERR_NULL_ARG,
ERR_OUT_OF_MEMORY,
ERR_IO,
ERR_INVALID_DATA,
} ErrorCode;
const char *error_message(ErrorCode e) {
switch (e) {
case ERR_OK: return "success";
case ERR_NULL_ARG: return "null argument";
case ERR_OUT_OF_MEMORY: return "out of memory";
case ERR_IO: return "I/O error";
case ERR_INVALID_DATA: return "invalid data";
default: return "unknown error";
}
}
int main(void) {
char *upper = duplicate_upper("hello");
if (upper) {
printf("%s\n", upper);
free(upper);
}
return 0;
}
errno, perror, and strerror
Standard library functions set the global variable errno when they fail. errno is a thread-local integer that contains a code describing the most recent error. Always check it immediately after a failing call — the next system call will overwrite it.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void) {
/* Open a non-existent file to trigger an error */
FILE *fp = fopen("/nonexistent/path/file.txt", "r");
if (!fp) {
/* errno is now ENOENT (no such file or directory) */
printf("errno = %d\n", errno);
/* perror: prints "prefix: human-readable message\n" to stderr */
perror("fopen failed");
/* strerror: returns the error string — useful for custom formatting */
printf("Error: %s\n", strerror(errno));
}
/* Reset errno before a call whose error you want to detect */
errno = 0;
long val = strtol("not_a_number", NULL, 10);
if (errno != 0) {
perror("strtol");
} else {
printf("val = %ld\n", val); /* 0 — partial conversion */
}
/* Check errno after math functions for domain errors */
#include <math.h>
errno = 0;
double result = sqrt(-1.0);
if (errno == EDOM) {
fprintf(stderr, "sqrt: domain error\n");
}
return 0;
}
Important rules for errno:
- Check it immediately after the failing call — the next system call will overwrite it
- Set
errno = 0before calls whose failure you want to distinguish from “no error” errnois thread-local in POSIX — safe to use in multi-threaded programs
Common errno values:
| Constant | Value | Meaning |
|---|---|---|
EPERM | 1 | Operation not permitted |
ENOENT | 2 | No such file or directory |
ENOMEM | 12 | Out of memory |
EACCES | 13 | Permission denied |
EEXIST | 17 | File exists |
EINVAL | 22 | Invalid argument |
ENOSPC | 28 | No space left on device |
Defensive Programming
Defensive programming means validating your assumptions explicitly, so bugs surface immediately at the point of the mistake rather than silently corrupting data and crashing somewhere unrelated later.
#include <assert.h>
#include <stddef.h>
/* assert: terminates with a message if condition is false.
Use for programming errors — conditions that should NEVER be false in correct code. */
double safe_divide(double a, double b) {
assert(b != 0.0 && "divisor must not be zero");
return a / b;
}
/* For runtime errors (user input, files, network) use proper error returns, not assert */
int parse_age(const char *s, int *out_age) {
if (!s || !out_age) return -1; /* null check — never trust callers */
char *end;
errno = 0;
long age = strtol(s, &end, 10);
if (errno != 0) return -1; /* overflow or underflow */
if (end == s) return -1; /* no digits found */
if (*end != '\0') return -1; /* trailing garbage like "25abc" */
if (age < 0 || age > 150) return -1; /* domain check */
*out_age = (int)age;
return 0;
}
/* Compile-time assertions catch platform assumptions at build time */
#include <stdint.h>
_Static_assert(sizeof(int) == 4, "int must be 4 bytes on this platform");
_Static_assert(sizeof(void*) >= 4, "pointer must be at least 4 bytes");
Error Propagation with goto Cleanup
When a function acquires multiple resources and any step can fail, the goto cleanup pattern provides a clean single exit point without duplicating cleanup code at every return path. This is the most widely accepted use of goto in C.
#include <stdio.h>
#include <stdlib.h>
typedef struct { int *data; size_t n; } IntArray;
int process_file(const char *filename, IntArray *out) {
FILE *fp = NULL;
int *buf = NULL;
int rc = -1;
fp = fopen(filename, "r");
if (!fp) { perror("fopen"); goto cleanup; }
int count;
if (fscanf(fp, "%d", &count) != 1 || count <= 0) {
fprintf(stderr, "Invalid count\n");
goto cleanup;
}
buf = malloc((size_t)count * sizeof(int));
if (!buf) { perror("malloc"); goto cleanup; }
for (int i = 0; i < count; i++) {
if (fscanf(fp, "%d", &buf[i]) != 1) {
fprintf(stderr, "Read error at index %d\n", i);
goto cleanup;
}
}
out->data = buf;
out->n = (size_t)count;
buf = NULL; /* transfer ownership — don't free in cleanup */
rc = 0; /* success */
cleanup:
free(buf); /* free(NULL) is safe — a no-op */
if (fp) fclose(fp);
return rc;
}
setjmp / longjmp
setjmp saves the current execution context (stack pointer, registers) and returns 0. longjmp restores that context and makes setjmp return a non-zero value, effectively jumping back in time to the setjmp call site. This implements non-local jumps across function call frames.
#include <stdio.h>
#include <setjmp.h>
static jmp_buf error_jump;
void deep_function(int level) {
printf("Entering level %d\n", level);
if (level == 0) {
fprintf(stderr, "Fatal error at level 0!\n");
longjmp(error_jump, 1); /* jump back to setjmp, which returns 1 */
}
deep_function(level - 1);
printf("Returning from level %d\n", level); /* never reached if longjmp fires */
}
int main(void) {
int err = setjmp(error_jump); /* returns 0 on first call */
if (err == 0) {
deep_function(3);
printf("deep_function completed normally\n");
} else {
fprintf(stderr, "Caught error (code %d) — aborting\n", err);
return 1;
}
return 0;
}
Caveats:
- Variables modified between
setjmpandlongjmpmust bevolatileto be reliably visible after the jump - All resources allocated between
setjmpandlongjmp(file handles, heap memory) are leaked — there is no automatic cleanup - Do not
longjmpout of a signal handler (undefined behavior) - Never use
longjmpto jump into a function (only out)
For most code, explicit error returns are cleaner and safer than setjmp/longjmp.