Two different kinds of 'something went wrong'
By now in this rung you have learned to check every call, to read the return code and errno each library function hands back, and to clean up on every exit path. Those skills make you good at noticing that something failed. This guide is about the question that comes the instant you notice: is this a failure I should handle, or a failure that means my program is broken? Those are two genuinely different things, and treating them the same way is one of the most common ways robust-looking code goes wrong.
A recoverable error is the world refusing your request for a reason that is nobody's fault. You call `open("/data/log.txt", O_RDONLY)` and it returns -1 with errno set to ENOENT — the file simply isn't there. You call `malloc(n)` and it returns NULL because the machine is out of memory. You try to connect a socket and the network is down. None of these means your code is wrong; they mean the environment did not cooperate. Your program anticipated this could happen, so it can respond sensibly: pick another file, free some memory and retry, tell the user the server is unreachable. The program stays in a sane state the whole time.
A bug is something else entirely. A bug is a violated assumption inside your own logic — a promise your code made to itself and then broke. You dereference a pointer that a function swore it would never return as NULL. You index `a[i]` where `i` has somehow grown past the array's length. You unlock a mutex you never locked. When one of these happens, the failure is not the environment saying no; it is proof that the program's model of reality is already false. And crucially, you usually cannot sensibly recover from it, because you no longer know which of your other assumptions still hold. The line between these two is the whole subject of this guide: it is the recoverable-error-versus-bug distinction.
Why the line matters: handle, propagate, or abort
The distinction is not philosophy for its own sake — it tells you what to do. For any failure you have exactly three responses, and which one is right depends entirely on which kind of failure it is. This is the handle, propagate, or abort choice. You handle an error when you are the layer that knows how to fix it (retry, fall back, substitute a default). You propagate it when you can't fix it but a caller above you might — you return a clean error code up the stack and let someone with more context decide. And you abort when continuing would be a lie: the program's invariants are broken, so the safest act is to stop loudly, now, before the damage spreads.
Here is the rule of thumb that ties it together. Recoverable errors are handled or propagated; bugs are aborted. The reason you abort on a bug rather than limp onward is the most counter-intuitive idea in this whole rung, so let it land: once an invariant is broken, every later line of your program is running on assumptions that may be false. A program that keeps going after detecting its own corruption does not become more robust — it becomes a machine that confidently produces wrong answers, or silently writes garbage to your database. Stopping is the responsible choice precisely because you have lost the ability to reason about what happens next.
A worked example: the same NULL, two meanings
The trap is that the same value can be a recoverable error in one place and a bug in another. NULL from `malloc()` is the textbook recoverable error: the heap is exhausted, which is an environmental fact, so you check it and propagate. But NULL arriving as a function argument that your contract said would never be NULL is a bug in the caller. Look at the two checks side by side and notice they are written differently on purpose.
// Recoverable error: out of memory is the world's no.
// We propagate a clean failure up to the caller.
char *dup_line(const char *src) {
char *buf = malloc(strlen(src) + 1);
if (buf == NULL) // expected: heap can be exhausted
return NULL; // PROPAGATE - caller decides what to do
strcpy(buf, src);
return buf;
}
// Bug: our contract says src is never NULL.
// If it IS, a caller broke the promise -> abort, do not 'handle'.
size_t safe_len(const char *src) {
assert(src != NULL); // invariant; firing here means a BUG
return strlen(src);
}Why not just `if (src == NULL) return 0;` in `safe_len()` to be 'safe'? Because that would be hiding a bug, not handling an error. If a caller passed NULL, your program already contains a logic mistake; returning 0 lets that mistake continue undetected and produce a subtly wrong answer far away, where it is a hundred times harder to find. This is the symptom-versus-root-cause gap again. The assertion that aborts gives you a crash at the broken contract, with a backtrace pointing at the guilty caller — vastly more useful than a wrong length silently flowing downstream.
Undefined behavior: the bug that won't even crash
There is a special, dangerous category of bug in C that you must hold separate in your mind: undefined behavior, or UB. Reading past the end of an array, dereferencing a dangling pointer, signed integer overflow — these are not 'errors the program detects and reports'. The C standard simply declines to say what happens, and that absence is not the same as 'crash'. Sometimes you get a clean segfault. Often you get nothing visible at all: the program reads a stale value, keeps running, and corrupts state you won't notice for hours.
Be exact about why UB is so treacherous, because the gentle slogans about it are wrong. UB does not mean 'whatever the platform happens to do'. It means the compiler is allowed to assume the UB never occurs and optimize on that assumption. So a check you wrote, like `if (p != NULL) ...` placed after you already dereferenced `*p`, can be deleted entirely by the optimizer: it reasons that since dereferencing `*p` would be UB if `p` were NULL, `p` must not be NULL, so the check is dead code. That is why a bug can behave perfectly at `-O0` and corrupt memory at `-O2` from the same source — the dreaded it-works-on-my-machine bug. UB is always a bug; it is never a recoverable error.
Classifying in practice, and how Rust hard-wires the line
When a failure crosses your code, run it through this quick triage to decide which kind it is and what to do:
- Ask: could this happen with completely correct code, just because the environment said no? (file missing, out of memory, network down, bad user input). If yes, it is a recoverable error.
- If it is recoverable: handle it here if you have the context to fix it; otherwise propagate a clean error code or sentinel up the stack to a caller who does.
- Ask instead: could this only happen if one of my own assumptions is already false? (a NULL the contract forbade, an index past the end, a state machine in an impossible state). If yes, it is a bug.
- If it is a bug: do not try to 'handle' it. Assert the invariant so it aborts at the source, and treat the abort as a defect to fix in code, not a condition to recover from at runtime.
This whole discipline is something you must impose by hand in C — the language won't remind you. It is worth seeing, by contrast, how Rust bakes the line right into the type system. Rust splits the two categories at the language level: recoverable errors are values of the Result type, `Result<T, E>`, which a function returns and the caller is forced by the compiler to deal with, while bugs trigger a `panic!` that unwinds and aborts — its version of fail-fast. You cannot accidentally ignore a `Result` and let an error vanish the way you can forget to check a C return code. That is a real, concrete benefit.
But hold this honestly, the way the rest of this ladder insists. Rust enforces the recoverable-versus-bug shape and frees you from many memory bugs in safe code, but it does not abolish bugs — a `panic!` is exactly Rust admitting your program hit a bug it cannot recover from, and you can still write a logic error that returns the wrong `Result`. The classification you are learning here is not a Rust feature; it is a way of thinking that good C programmers apply by discipline and that Rust merely makes harder to ignore. Learn the thinking, and you write more robust code in any language.