Error Handling & errno

C has no exceptions. Failure is a value you must check: functions return status codes, and many library calls also set the global 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.

errnoMeaningTypical cause
ENOENT (2)No such file or directoryfopen of a missing path
EACCES (13)Permission deniedOpening a read-only file for write
EINVAL (22)Invalid argumentBad parameter to a syscall
ENOMEM (12)Out of memorymalloc failure
ERANGE (34)Result out of rangestrtol overflow
EAGAIN (11)Try againNon-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:

  1. Check every fallible call. If a function can fail, its return value exists for a reason — you read it or the failure is silent.
  2. Propagate with context. Add the operation name and any relevant values to the message: fprintf(stderr, "read %s: %s\n", path, strerror(errno)).
  3. Clean up on every path. An error return must free what was allocated and close what was opened — same as the success path.
  4. 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.