← Prism
Docs
Overview defer orelse zeroinit raw auto-unreachable
Spec Draft
GitHub ↗

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.

Standard C — 4 exit points, each manually correct
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;
}
With defer — write cleanup once
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:

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:

Use -fno-defer to disable defer checking for an entire file.

Spec Reference

defer — Prism Spec §6.1