defer
Write cleanup once, immediately after acquisition. It runs on every exit path — return, goto, break, or block end. No more cleanup ladders.
Overview
The core problem with resource management in C: every acquisition needs a corresponding release, and every early exit needs its own copy of that release. Add three resources and every error path must release all three in the right order. Miss one and you leak.
int compile(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return -1;
char *src = read_file(f);
if (!src) { fclose(f); return -1; }
Token *tok = tokenize(src);
if (!tok) { free(src); fclose(f); return -1; }
int r = emit(tok);
token_free(tok);
free(src);
fclose(f);
return r;
}
int compile(const char *path) {
FILE *f = fopen(path, "r")
orelse return -1;
defer fclose(f);
char *src = read_file(f)
orelse return -1;
defer free(src);
Token *tok = tokenize(src)
orelse return -1;
defer token_free(tok);
return emit(tok);
}
Every early return automatically runs all registered defers in reverse order. No manual bookkeeping, no missed cleanup.
Syntax
Two forms — block and single statement:
// Block form — for multi-statement cleanup
defer {
fclose(f);
free(buf);
}
// Single-statement form
defer fclose(f);
defer free(buf);
defer may appear anywhere inside a function body — at the top level, inside if/else/for/while blocks, or nested blocks. It may not appear at file scope.
Behavior
The deferred statement runs when its enclosing scope exits, triggered by any of:
return— function returns (all defers from current scope to function root run)}— reaching the closing brace of the block containing the defergoto— if the jump leaves the scope containing the deferbreakorcontinue— if they exit the scope containing the defer
LIFO order: Multiple defers in the same scope run in reverse registration order — last registered fires first. This naturally handles dependent resources (close before free, unlock before destroy).
void example(void) {
defer c_cleanup(); // registered 1st — fires 3rd
defer b_cleanup(); // registered 2nd — fires 2nd
defer a_cleanup(); // registered 3rd — fires 1st
// on exit: a_cleanup(), b_cleanup(), c_cleanup()
}
Scope-local: A defer in an inner block runs when that block exits, not at function return.
for (int i = 0; i < n; i++) {
char *tmp = alloc_item(i) orelse continue;
defer free(tmp); // runs at end of each iteration
process(tmp);
}
Examples
File I/O
int write_config(const char *path, const char *data) {
FILE *f = fopen(path, "w") orelse return -1;
defer fclose(f);
if (fputs(data, f) == EOF) return -1; // fclose runs
return 0; // fclose runs
}
Mutex lock/unlock
int update_counter(struct State *s, int delta) {
pthread_mutex_lock(&s->lock);
defer { pthread_mutex_unlock(&s->lock); }
if (s->count + delta < 0) return -1; // unlock runs
s->count += delta;
return 0; // unlock runs
}
File descriptor
ssize_t read_all(const char *path, char *buf, size_t len) {
int fd = open(path, O_RDONLY) orelse return -1;
defer close(fd);
return read(fd, buf, len);
}
Temporary directory cleanup
int build_package(const char *src) {
char tmpdir[] = "/tmp/pkg-XXXXXX";
if (!mkdtemp(tmpdir)) return -1;
defer { rm_rf(tmpdir); } // always cleaned up
if (copy_sources(src, tmpdir) != 0) return -1;
if (run_build(tmpdir) != 0) return -1;
return install_output(tmpdir);
}
Nested blocks — inner scope cleanup
int process_batch(int *items, int n) {
int errors = 0;
for (int i = 0; i < n; i++) {
Handle *h = acquire(items[i]);
if (!h) { errors++; continue; }
defer release(h); // runs at end of each loop iteration
errors += process_one(h) != 0;
}
return errors;
}
Patterns & Tricks
Loop cleanup — per-iteration defers
When defer appears inside a loop, a new defer is registered on each iteration. It fires at the end of that iteration's scope, not accumulated to loop exit. This eliminates nested cleanup ladders and enables simple per-iteration resource management:
for (int i = 0; i < count; i++) {
Resource *r = acquire(i) orelse continue;
defer release(r); // fires at end of iteration i, not loop exit
process(r);
}
Compare this to standard C: without defer, you'd need a goto-based cleanup ladder or repeated cleanup calls on every exit path inside the loop.
Guard pattern — atomic multi-resource acquisition
When acquiring multiple dependent resources, register a defer immediately after each success. If any later acquisition fails, all previous defers run in reverse order automatically:
int setup(struct System *sys) {
if (!open_db(sys)) return -1;
defer close_db(sys);
if (!open_network(sys)) return -1; // if fails: db closes via defer
defer close_network(sys);
if (!spawn_threads(sys)) return -1; // if fails: network + db close in LIFO order
defer stop_threads(sys);
return 1; // full success
}
// On any failure, cleanup is: stop_threads → close_network → close_db (reverse order)
This pattern is why defer is so powerful for systems programming: partial failures are always handled correctly without manual bookkeeping.
Ownership transfer — when NOT to defer
If your function returns ownership of a resource to the caller (like returning an fd), you must not register a defer. Instead, handle cleanup on error paths explicitly:
int open_device(const char *path) {
int fd = open(path, O_RDWR) orelse return -1;
if (set_nonblock(fd) != 0) {
close(fd); // explicit cleanup on error
return -1;
}
return fd; // fd is live; caller owns it. No defer here.
}
If you add defer close(fd), the fd will be closed when the function returns, breaking the caller's handle.
Conditional cleanup — flags and guards
Sometimes you need cleanup only if a later operation succeeded. Use a flag variable:
int process_file(const char *path) {
FILE *f = fopen(path, "r");
if (!f) return -1;
int did_lock = 0;
if (acquire_lock(f) != 0) {
fclose(f);
return -1;
}
did_lock = 1;
defer {
if (did_lock) release_lock(f);
fclose(f);
}
// ... work with locked file ...
return 0;
}
Edge Cases
defer inside loops
A new defer is registered on each iteration. It fires at the end of that iteration's scope — not accumulated to function exit.
for (int i = 0; i < 100; i++) {
void *p = malloc(64) orelse break;
defer free(p); // freed at end of this iteration, not after the loop
use(p);
}
defer + goto
Jumping out of a scope with active defers is fine — Prism emits the cleanup before the jump. Jumping into a scope past a defer is a compile error.
// OK — goto out of defer scope
if (open_resource()) {
defer close_resource(); // emitted before goto
if (error) goto done;
use_resource();
}
done:
return 0;
Statement expressions
defer inside a GNU statement expression ({…}) is scoped to that statement expression, not the outer function. The deferred code runs when the ({…}) expression completes. Additionally, a block containing defer must not be the last statement of the statement expression — place the defer before the final expression.
Forbidden Contexts
Prism rejects defer in functions where cleanup cannot be guaranteed:
setjmp/longjmp—longjmpbypasses the normal call stack, making defers silently skip. Any appearance of these names in the function body taints it. Hard error.vfork— the child and parent share the stack; running defers in the child corrupts the parent's state. Any reference tovfork(including bare references likefp = vfork) taints the function. Hard error.asm goto— computed labels in assembly jump to targets Prism cannot see at compile time. Regularasm volatileis safe and allowed. Hard error.- Computed goto with active defers —
goto *ptrhas an unresolvable jump target, so cleanup path is unknown. Hard error. return,goto,break,continueinside defer bodies — the defer body must not control outer flow. Hard error.
Use -fno-defer to disable defer checking for an entire file.