Acquire four resources, and any of the acquisitions can fail. In a language
with destructors or defer, cleanup is automatic. In C you have three
options: nest ifs four deep and watch the happy path drift off the right
margin; write the cleanup four times in four early returns and leak the day
someone edits one of them; or use the word your professor banned. The Linux
kernel — 30+ million lines whose error paths get fuzzed by the planet —
chose door three, uniformly, on purpose. The pattern deserves to be taught
as the C idiom rather than whispered as an exception to a rule.
The Shape
Labels stack in reverse order of acquisition, and each failure jumps to the label that unwinds exactly what’s held so far:
int device_init(struct device *dev)
{
int err;
err = alloc_buffers(dev);
if (err)
return err; /* nothing held yet: plain return */
err = register_irq(dev);
if (err)
goto free_buffers;
err = create_sysfs(dev);
if (err)
goto unregister_irq;
err = start_thread(dev);
if (err)
goto remove_sysfs;
return 0;
remove_sysfs:
remove_sysfs(dev);
unregister_irq:
unregister_irq(dev);
free_buffers:
free_buffers(dev);
return err;
}Read what the structure gives you: the happy path is a straight line at one
indent level; each acquisition pairs visually with its release, in stack
order (release order is reversed by falling through the labels — the
buggy version jumps past cleanups, and reviewers can check pairing by eye);
adding a fifth resource is one if and one label, not a re-audit of four
return paths. This is manual RAII — same discipline as destructors, with the
lifetimes spelled out. Dijkstra’s essay was about goto as control flow
spaghetti; forward-only jumps to a cleanup epilogue are the opposite of
spaghetti, and the kernel’s style guide says so in as many words.
The named-label variant above beats the goto fail/single-label variant
(cleanup guarded by ifs on which pointers are non-NULL) in review-ability —
though initializing pointers to NULL and making free functions NULL-safe
(like free itself, and kfree) keeps even that variant honest. And yes,
Apple’s actual goto fail; TLS bug is the counterexample people cite — but
that was a duplicated line creating an unconditional jump, a bug braces
and -Wmisleading-indentation catch, not an indictment of the idiom.
The Other Half: Reporting Errors
Cleanup is half of C error handling; the other half is the return
convention, and mixing conventions is its own bug class. The choices that
coexist in every codebase: return 0/-1 and set errno (POSIX syscalls),
return negative error codes directly (-EINVAL — the kernel, which cannot
use errno), return NULL for pointer-yielding functions (losing the which
error — hence the kernel’s ERR_PTR/PTR_ERR trick of smuggling the code
inside the invalid-pointer range), or out-parameters for the result with the
return reserved for status. Pick per-project, document it, and never let a
function silently swallow a code — the grep-resistant bug is
if (do_thing(x) < 0) return -1; discarding which failure for callers
who needed to distinguish EAGAIN from ENOMEM.
Two closing notes from the tooling era. GCC/Clang’s
__attribute__((cleanup)) gives C actual scope-based destructors (systemd
builds its whole _cleanup_free_ idiom on it; C2y-era discussions of
defer aim to standardize the shape) — genuinely good, though it makes
cleanup order implicit again, which is what the labels were showing you. And
regardless of idiom, the error paths are the least-executed, least-tested
code you own: fault-injection (failing mallocs under test) plus ASan on the
suite is how the goto ladders earn their keep — a cleanup path that never
ran in CI is a rumor, not a guarantee.
Takeaways
- Stacked goto labels in reverse acquisition order are C’s RAII: straight happy path, pairwise-auditable cleanup, one-label cost per new resource.
- Fall through the labels; jumping past a cleanup is the bug pattern. NULL-init plus NULL-safe frees keep partial paths honest.
- Pick one error-reporting convention (errno vs negative codes vs ERR_PTR) and never discard the specific code on the way up.
- attribute((cleanup)) is real scope-based cleanup for C when available; the label ladder remains the portable baseline.
- Error paths are cold code: fault-inject them in CI or assume they leak.