CFoundations

C Error Handling & errno: Defensive Programming for Industrial-Grade Systems

TT
TopicTrick Team
C Error Handling & errno: Defensive Programming for Industrial-Grade Systems

C Error Handling & errno: Defensive Programming for Industrial-Grade Systems


Table of Contents


Why C Has No Exceptions

In languages with exceptions (Java, Python, C++), when a function fails, it throws an object that unwinds the call stack automatically, calling destructors and freeing resources. C provides none of this.

This is a deliberate design choice, not an oversight:

  1. Exception unwinding has non-trivial runtime overhead (exception tables, dynamic dispatch).
  2. Real-time systems need deterministic execution — no hidden stack unwinding at unpredictable times.
  3. Kernel code runs in a context where many OS services used for exception handling are unavailable.
  4. C's philosophy: the programmer controls everything explicitly.

The result is that C error handling is explicit: every call to a fallible function is followed by an explicit check, and every failure path is an explicit code path. This makes C error handling verbose, but also perfectly predictable and auditable.


Return Codes: The Primary Mechanism

mermaid

The C convention (used by the POSIX standard and the Linux kernel API):

  • Functions return 0 or positive value on success.
  • Functions return -1 on failure (integer functions) or NULL (pointer functions).
  • After failure, errno is set to indicate the specific error.
c

errno: The Global Error Register

errno is defined in <errno.h> as a macro expanding to a modifiable lvalue. In modern C (thread-safe implementations), it is actually a thread-local variable — each thread has its own independent errno.

c

[!IMPORTANT] Save errno immediately after the failed call. The very next library function called (even fprintf) can overwrite errno. Always copy it to a local variable before any other function call.


perror and strerror: Human-Readable Errors

Two standard functions convert numeric error codes to descriptive strings:

c

The goto Cleanup Pattern: Resource Safety

This is the most important C error handling pattern. Without it, error paths lead to either deeply nested if-statements or duplicated cleanup code:

c

Properties of this pattern:

  • Resources are always acquired in forward order and released in reverse order.
  • No resource is freed twice — each label is reached only if the preceding resource was successfully acquired.
  • The main logic reads naturally without nested if-statements ("arrow code").
  • All error paths lead to the same cleanup labels, enforcing DRY code.

Custom Result Types: Error + Value in One Return

For APIs that need to return both a value and an error status, define a tagged result type:

c

This pattern is similar to Rust's Result<T, E> type — a well-typed alternative to errno-based error communication.


Error Propagation Discipline

In multi-layer systems, errors must propagate upward:

c

The Linux kernel convention uses negative errno values (-ENOENT, -ENOMEM) propagated upward through call chains, converted to user-space positive errno only at the syscall boundary.


setjmp and longjmp: Non-Local Error Handling

setjmp/longjmp from <setjmp.h> enable jumping across multiple stack frames — the only C mechanism for something resembling exception handling:

c

[!CAUTION] longjmp is dangerous: it does NOT call destructors, does NOT flush file buffers, and does NOT free heap memory allocated after the setjmp. Resources acquired between setjmp and longjmp are leaked. Use only when the consequence of longjmp is controlled failure termination, not recovery with continued operation.


Defensive Assertions with abort()

For programmer errors (violated invariants, impossible states) — not user input errors — use assertions that call abort():

c

Compile with -DNDEBUG to disable assertions in release builds (same as C's standard assert() macro from <assert.h>).


Real-World Case Study: Linux System Call Error Handling

The Linux kernel and POSIX system calls exemplify production-grade C error handling:

c

Frequently Asked Questions

Is it safe to use errno in multi-threaded code? Yes — modern POSIX implementations make errno thread-local (each thread has its own copy). However, always save errno to a local variable immediately after a failed call, before calling any other function that might overwrite it.

When should I use perror vs fprintf(stderr, strerror(errno))? Use perror("operation_name") for quick diagnostics — it automatically prepends the string and appends the colon and error description. Use strerror(errno) when you need to include the error in a larger message, log it with additional context, or use a non-global errno value (e.g., one you saved earlier).

Is there a standard pattern for custom error codes? Define error codes as negative numbers (Linux kernel style) or as an enum with a specific range. Mixing your error codes with POSIX errno values (always positive) allows consistent signed-return-code APIs.

Can I recover from abort()? No. abort() sends SIGABRT to the process, which terminates it immediately (after writing a core dump if enabled). It is intended for unrecoverable programmer errors, not user-input errors. For recoverable errors, return error codes or use setjmp/longjmp.

What do I do if malloc fails in deeply nested code? Option A: Propagate -ENOMEM back up through every return value (most robust). Option B: Use setjmp/longjmp with a top-level error handler. Option C: Use a pre-allocated memory pool/arena allocator that never fails after initialization. Option A is preferred in library code; C is often acceptable in application code with careful resource management.


Key Takeaway

C error handling is Explicit Discipline. Every fallible call must be checked; every resource must be tracked; every failure path must be clean. The goto cleanup pattern enforces resource safety. errno and strerror make system errors human-readable. Custom result types make APIs self-documenting.

This explicit discipline produces code that survives hardware failures, resource exhaustion, and adversarial conditions — which is exactly why C is the language of choice for operating systems, database engines, and anything that must operate continuously for months or years without crashing.

Read next: Binary Trees & Recursive Algorithms in C →


Part of the C Mastery Course — 30 modules from C basics to expert systems engineering.