a recoverable error versus a bug
Imagine running a shop. A customer paying with a card that gets declined is an everyday, expected situation - you have a polite script for it and the shop carries on. But discovering that your cash register has been silently adding numbers wrong for a week is not a 'situation' - it is a defect in the machine, and the right response is to stop using it and fix it, not to keep ringing up sales. These are two fundamentally different kinds of 'something went wrong', and confusing them is one of the most common errors in handling errors.
A recoverable error is a failure that comes from the outside world and that your program can legitimately expect and cope with: a file that does not exist, a network connection that drops, a disk that is full, a user who types letters where a number was wanted. These are not your program's fault; they are facts about the environment. The correct response is to handle them - report the problem, retry, fall back, ask the user, or pass the failure up to a caller who can decide. A bug (also called a programmer error or a defect) is a flaw in your own code's logic: dereferencing a pointer that your code's own invariants say can never be NULL, indexing past the end of an array, reaching a switch case you believed was impossible. The correct response to a bug is not to 'recover' - there is nothing valid to recover to, because the program is now in a state you proved could never happen. The honest move is usually to fail fast: stop immediately (often via an assertion or abort()) so the bug is caught loudly and the corruption spreads no further.
Why this distinction is load-bearing: it tells you which tool to reach for. Recoverable errors call for error-checking code (return codes, errno, Result types) and graceful handling. Bugs call for assertions during development and a clean, loud crash in production - because trying to 'handle' a bug means writing code for a situation you do not understand, which usually just hides the corruption and makes the eventual failure harder to diagnose. The classic mistake is using an assertion to validate user input (treating a recoverable error as a bug, so it vanishes in release builds), or wrapping a null-pointer dereference in a try/catch and continuing (treating a bug as recoverable, so the program limps on with broken state).
fopen() returning NULL because a config file is missing is a recoverable error: report it and maybe fall back to defaults. A function receiving a NULL pointer that its own contract says is impossible is a bug: assert it and crash loudly in testing, rather than papering over it and corrupting state.
Outside-world failures get handled; broken-invariant failures get caught and stopped.
The same symptom (a NULL pointer, a -1) can be either, depending on whose fault it is and whether your code expected it. Ask: is this the environment misbehaving (handle it) or my own logic being wrong (a bug - fail fast)? Mislabelling drives the two classic mistakes above.