# Error Handling in C: In Defense of goto cleanup

> C has no destructors, no defer, no exceptions — just early returns that leak and nested ifs that march off the screen. The kernel settled this decades ago: goto-based cleanup is the correct idiom, and it's more disciplined than its reputation.

URL: https://harshith.in/blog/goto-cleanup-error-handling-c/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: systems, fundamentals

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 `if`s 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.
