Error Handling & errno
errno with a precise reason. This page teaches the return-code contract, the errno vocabulary, assertions for invariants, and the escape hatch setjmp/longjmp.
The Return-Code Contract
Well-designed C functions signal success or failure through their return value, and document both. The convention is zero/positive for success, a distinct code for each failure. Programs do the same with exit status — a shell script can then react to your program's outcome.
#include <stdio.h>
#include <string.h> // strlen
// contract: returns 0 on success, -1 if text is empty or too long
int log_line(const char *text) {
if (text == NULL || text[0] == '\0') {
return -1; // nothing to log — fail fast
}
if (strlen(text) > 1024) {
return -1; // contract violation: too long
}
fprintf(stdout, "[log] %s\n", text);
return 0; // success
}
int main(void) {
if (log_line("") != 0) { // every call must be checked
fprintf(stderr, "logging failed\n");
return 2; // program exit status: failure
}
return 0; // exit status 0: success
}
Common main exit conventions: 0 success, 1 generic failure, 2 usage error (what grep and friends use). Shell scripts test these with $?.
errno — The System's Reason
Many standard library calls set errno (declared in <errno.h>) to a small positive number describing why an operation failed. It is a global, so read it immediately after the failing call and before any further library work. Convert it to text with strerror or print it directly with perror, which prefixes your message.
| errno | Meaning | Typical cause |
|---|---|---|
ENOENT (2) | No such file or directory | fopen of a missing path |
EACCES (13) | Permission denied | Opening a read-only file for write |
EINVAL (22) | Invalid argument | Bad parameter to a syscall |
ENOMEM (12) | Out of memory | malloc failure |
ERANGE (34) | Result out of range | strtol overflow |
EAGAIN (11) | Try again | Non-blocking read with no data |
#include <errno.h> // errno, ENOENT, ERANGE...
#include <stdio.h>
#include <string.h> // strerror
int main(void) {
errno = 0; // clear before the risky call
FILE *f = fopen("does_not_exist.txt", "r");
if (f == NULL) {
printf("code: %d\n", errno); // 2 (ENOENT)
printf("text: %s\n", strerror(errno)); // "No such file..."
perror("open"); // "open: No such file..."
return 1;
}
fclose(f);
return 0;
}
Note the pattern: reset errno to 0 before calling, because it is never cleared by success — a stale value from an earlier call is the classic false-positive.
Assertions — Catching the Impossible
assert(cond) from <assert.h> documents an invariant and aborts the program with file/line details when development finds it broken. It is compiled out entirely if NDEBUG is defined — so use it for programmer assumptions ("this must never happen"), never for runtime error handling you still need in production.
#include <assert.h> // assert
#include <stdio.h>
int divide(int a, int b) {
assert(b != 0); // contract: caller must not pass zero
return a / b; // without the assert, this is UB on b==0
}
int main(void) {
printf("%d\n", divide(10, 2)); // 5
// divide(10, 0); // aborts with "assert failed" in debug
return 0;
}
setjmp / longjmp — Non-Local Jumps
setjmp books a place in the call stack; a later longjmp unwinds the stack back to it — a controlled, non-local jump that skips all intermediate returns. This is C's closest relative to exceptions, used mainly in embedded code and some libraries for emergency recovery. It is powerful and dangerous: local variables of skipped frames whose values changed may be indeterminate, and resources allocated in between leak unless you track them.
#include <setjmp.h> // jmp_buf, setjmp, longjmp
#include <stdio.h>
jmp_buf env; // the saved execution point
// pretend a deep operation that must abort the whole transaction
void risky_step(int step) {
if (step > 3) {
longjmp(env, 42); // jump back to setjmp, return value 42
}
}
int main(void) {
int status = setjmp(env); // 0 first time; longjmp's value if called back
if (status != 0) {
printf("aborted at step with code %d\n", status); // recovery here
return 1;
}
for (int s = 1; s < 10; s++) {
risky_step(s); // when s exceeds 3, control returns to setjmp
}
printf("completed\n");
return 0;
}
Use setjmp/longjmp sparingly — prefer clean return-code propagation, and always free any resources acquired between setjmp and the jump.
Error-Handling Discipline
Four rules turn C's primitive error machinery into a reliable system:
- Check every fallible call. If a function can fail, its return value exists for a reason — you read it or the failure is silent.
- Propagate with context. Add the operation name and any relevant values to the message:
fprintf(stderr, "read %s: %s\n", path, strerror(errno)). - Clean up on every path. An error return must free what was allocated and close what was opened — same as the success path.
- Let it fail loudly. In debug builds, assert invariants and run the sanitizers; an early crash with a stack trace beats a late silent corruption.
Next: strings — the char-array frontier where C's most famous bugs live.