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:

c
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.