JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

Cleaning Up: goto-Cleanup and RAII

Checking every call was only half the job; the other half is undoing what you acquired when a later call fails. This guide walks the cleanup problem from the bug it breeds to its two great answers — the goto-cleanup pattern that disciplined C uses, and the RAII idea that C++ and Rust bake into the language itself.

The leftover from checking every call

The previous guide drilled one habit into you: check every call that can fail. You learned to read the return value and errno, to compare against the failure sentinel, and to branch instead of barging ahead. But checking a call only tells you that something failed — it hands you a fork in the road, not a clean way back. The moment a function does more than one thing, that fork sprouts a second, harder problem, and this guide is about taming it.

Here is the picture. A realistic function acquires several things in sequence: it opens a file, then allocates a buffer on the heap, then takes a lock, then does its work. Each of those is a call you dutifully checked. But every acquisition you survive becomes a debt: a file descriptor you must later close(), heap memory you must later free(), a mutex you must later unlock. This is the heart of resource management — pairing every acquire with exactly one matching release, on every path the function can take.

The catch is that any step in the middle can fail, and when it does you must undo exactly what you have acquired so far — no more, no less. If the lock fails, you must free the buffer and close the file, but you must not unlock, because you never locked. If the buffer allocation fails, you must close the file but not free a buffer that was never allocated. Get this wrong by under-cleaning and you leak a descriptor, some memory, or a held lock; get it wrong by over-cleaning and you free something twice or unlock a mutex you never held — and that is straight undefined behavior.

The naive fix, and why it rots

The first instinct of almost everyone is to clean up right where the failure is found. You open the file; if malloc() fails, you close the file and return right there; if the lock fails, you free the buffer and close the file and return right there. It works — for about a week. The cleanup code is now copy-pasted across several return points, each copy a slightly different prefix of the same release sequence, and the copies drift apart as the function grows.

The other instinct is to nest: an `if (open ok) { if (malloc ok) { if (lock ok) { ... } } }`, with the cleanup tucked after each closing brace. This at least keeps each release in one place, but it marches your real logic ever rightward into a deep staircase of indentation — four acquisitions and the actual work is buried four levels deep. Both shapes are honest attempts, and both scale badly: the copy-paste version duplicates cleanup, the nested version duplicates structure. C gives you no destructor, no `finally` block, no automatic anything — so neither approach can lean on the language for help.

So idiomatic C reaches for a tool you were probably warned never to touch. The clean answer is to send every failure path to one shared exit at the bottom of the function, where the releases live in a single, easy-to-audit place. And the way C jumps to that one place is the keyword `goto`.

goto-cleanup: one back door for everyone

Picture a building with several doors, but a rule that everyone leaves through the same back door, where the lights, the alarm, and the locks all get handled in one place, correctly, every time. The goto-cleanup pattern applies exactly this to a function. Instead of scattering cleanup at every early return, you label a cleanup section at the bottom and `goto` it on failure. The labels are stacked in reverse order of acquisition, and each label does its own one release and then falls through into the next — so jumping to the right label undoes precisely what was acquired so far. This is the disciplined use of `goto` you will find recommended throughout real C, including the Linux kernel.

int do_work(const char *path) {
    int   rc = -1;          /* assume failure until proven otherwise */
    int   fd = -1;          /* -1 means "no descriptor acquired"     */
    char *buf = NULL;       /* NULL means "no buffer acquired"        */

    fd = open(path, O_RDONLY);
    if (fd == -1)           goto out;          /* nothing to undo yet */

    buf = malloc(4096);
    if (buf == NULL)        goto close_fd;     /* only the fd is open */

    if (read(fd, buf, 4096) == -1)  goto free_buf;  /* fd + buf live  */

    /* ... real work here ... */
    rc = 0;                 /* success */

free_buf:
    free(buf);              /* free(NULL) is safe, so this is fine    */
close_fd:
    close(fd);              /* fd is -1 only if we jumped before open */
out:
    return rc;
}
The skeleton: labels stacked in reverse order of acquisition, each releasing one resource and falling through to the next; variables pre-initialised to safe 'nothing acquired' values.

Read the code as a stack being unwound. If open() fails, you `goto out` and skip every release — correct, because you hold nothing. If malloc() fails, you `goto close_fd`, which closes the file but never touches the buffer. If read() fails, you `goto free_buf`, which frees the buffer and then falls through into close_fd to close the file. On success you fall off the bottom and run the full cleanup too — exactly right, since the happy path also wants the file closed and the buffer freed. One release site per resource makes it trivial to verify that nothing leaks and nothing is freed twice. Stay honest about the limits, though: this is structured goto — always forward, only to cleanup labels, never a licence for spaghetti jumps — and it still leans entirely on you to keep the label order in sync with the acquisition order. It removes the duplication, not the responsibility. And it hands off cleanly to the next idea: when the function fails it returns rc, so its caller can decide whether to handle, propagate, or abort, with every resource already released on the way out.

RAII: make leaving the room lock the door

goto-cleanup is C bravely working around a missing language feature. C++ and Rust supply the feature, and it changes everything. Imagine a hotel keycard that locks your room the instant you step out — you cannot forget to lock up, because leaving is locking. That is RAII, which stands for resource acquisition is initialization (you pronounce it by spelling the letters out). The idea: tie a resource's lifetime to a variable's scope, so that when the variable goes out of scope — by any exit, normal return or error alike — the resource is released automatically.

The mechanism in C++ is the destructor. You write a small class that owns a resource: its constructor acquires it (opens the file, allocates the memory, takes the lock) and its destructor releases it (closes, frees, unlocks). When you create such an object as a local variable, the compiler guarantees the destructor runs the moment control leaves the scope — whether you reach the closing brace, hit a `return`, or an exception unwinds through the function. Cleanup is no longer something you remember to write at each exit; it is attached to the object and happens automatically, exactly once. The std::lock_guard is the textbook case: construct it and the lock is taken; let it fall out of scope and the lock is released, even if an error bails out three lines later.

Rust takes the same idea and welds it into the language as ownership plus the Drop trait. Every value has exactly one owner; when that owner goes out of scope, Rust automatically runs the value's cleanup — this is called drop. For a String or a Vec the drop frees the heap memory; for a file handle it closes the file; for a MutexGuard it releases the lock. You do not write `free` and you usually do not write `drop` — the compiler emits the call at the closing brace where the owner dies, in reverse order of declaration. Because ownership guarantees a single owner, drop runs exactly once at one well-defined moment, so there is no double-free and nothing to forget.

Same problem, two honest tradeoffs

Lay the two answers side by side and the tradeoff is clean. In C, the cleanup problem is a problem you solve, every function, by hand, with goto-cleanup and initialised sentinels and labels kept in sync. In C++ and Rust, the cleanup problem is largely dissolved by the language: there is no separate cleanup label to keep in step with acquisitions, and no risk that a newly added early return forgets to release, because release is the destructor's or drop's job, not yours. This is one of the strongest arguments for those languages over plain C in resource-heavy systems code — and it is the practical pay-off of Rust's ownership model.

So carry both tools, and know which world you are in. Writing C, reach for goto-cleanup the moment a function acquires more than one resource — it is the idiom that keeps your error paths honest and your releases auditable. Writing C++ or Rust, lean on RAII and ownership so the compiler discharges the obligation for you, and spend your attention instead on designing ownership correctly. Either way, the principle underneath is identical and it is the spine of this whole rung: every resource you acquire must be released on every path out, and the bugs live in the paths you forgot.