The goto cleanup Pattern in C¶
The Problem: Cascading Cleanup¶
In C, functions that acquire multiple resources (memory, file handles, GL objects) face a classic dilemma: how to release everything correctly when a late step fails. The naive approach — cascading free() calls at each error site — leads to O(n²) maintenance cost and is a proven source of memory leaks.
/* ❌ Anti-pattern: cascading free — O(n²) lines, leak-prone */
int init(App* app)
{
app->a = calloc(1, sizeof(*app->a));
if (!app->a) return 0;
app->b = calloc(1, sizeof(*app->b));
if (!app->b) {
free(app->a); /* 1 free */
return 0;
}
app->c = calloc(1, sizeof(*app->c));
if (!app->c) {
free(app->b); /* 2 frees */
free(app->a);
return 0;
}
/* Each new allocation adds N frees to EVERY error path above */
return 1;
}
Worse: when later operations fail (GL context creation, subsystem init), developers often write return 0 with zero cleanup, silently leaking all previously allocated resources.
The Solution: Centralized Exit via goto¶
A single cleanup block at the end of the function, reached by goto, guarantees that every error path releases the same resources in the same order:
/* ✅ goto cleanup — O(n) lines, no leaks possible */
int init(App* app)
{
app->a = calloc(1, sizeof(*app->a));
if (!app->a) goto cleanup;
app->b = calloc(1, sizeof(*app->b));
if (!app->b) goto cleanup;
app->c = calloc(1, sizeof(*app->c));
if (!app->c) goto cleanup;
return 1;
cleanup:
free(app->c); /* free(NULL) is safe (C99 §7.20.3.2) */
free(app->b);
free(app->a);
return 0;
}
Why free(NULL) Makes This Safe¶
The C standard guarantees that free(NULL) is a no-op. When calloc zero-initializes the struct, any pointer that was never assigned remains NULL and can be safely freed. This eliminates the need for if (ptr) free(ptr) guards.
Applied: app_init() in suckless-ogl¶
The app_init() function uses a single-label goto cleanup_full pattern:
| Label | When Used | Cleanup Action |
|---|---|---|
cleanup_full |
Failures after subsystem descriptor init (GL active) | Calls app_cleanup() which handles partial teardown |
The previous cleanup_alloc label (for pre-GL allocation failures) was removed when subsystem descriptors took over. The descriptor table's app_subsystems_init() now handles rollback automatically: on the first failure at index N, it calls cleanup() in reverse for entries [N-1 .. 0], freeing any calloc'd sub-structs. Only post-descriptor Phase 3 failures (scene, async loader, post-processing) still use goto cleanup_full.
Comparison¶
| Aspect | Cascading Free | goto cleanup |
|---|---|---|
| Lines of cleanup code | O(n²) | O(n) |
| Risk of missed free | High (each error path manual) | None (single path) |
| Adding a new allocation | Touch every error path above | Add one free() to cleanup block |
| Readability | Cluttered with repetitive frees | Clean: error → goto, cleanup at bottom |
References¶
- Linux kernel coding style §7 — kernel.org: "The goto statement comes in handy when a function exits from multiple locations and some common work such as cleanup has to be done."
- SEI CERT C MEM12-C — wiki.sei.cmu.edu: "Consider using a goto chain when leaving a function on error when using and releasing resources."
- Donald Knuth — "Structured Programming with go to Statements" (1974, Computing Surveys): formal analysis showing
gotofor cleanup is a valid, structured use. - Eli Bendersky — "Uses of goto in C": practical analysis of
gotofor resource cleanup in real C projects. - Linux Device Drivers, 3rd Ed. — O'Reilly: documents
gotocleanup as standard practice in kernel moduleinit/exitfunctions. - Stack Overflow — "Valid use of goto for error management in C": community consensus with 500+ upvotes endorsing the pattern.