Skip to content

Defer and Cleanup

Many programs need to clean something up after using it.

Many programs need to clean something up after using it.

A program may need to:

close a file

free memory

unlock a mutex

restore a setting

print a final message before leaving a scope

Zig gives you defer for this.

defer means: run this statement when the current scope ends.

A First Example

const std = @import("std");

pub fn main() void {
    std.debug.print("first\n", .{});

    defer std.debug.print("third\n", .{});

    std.debug.print("second\n", .{});
}

This prints:

first
second
third

The deferred statement is written before the second print:

defer std.debug.print("third\n", .{});

But it runs at the end of the scope.

The current scope here is the body of main.

Scope Is the Key

defer runs when the scope ends.

const std = @import("std");

pub fn main() void {
    std.debug.print("A\n", .{});

    {
        defer std.debug.print("C\n", .{});
        std.debug.print("B\n", .{});
    }

    std.debug.print("D\n", .{});
}

This prints:

A
B
C
D

The defer is inside the inner block. It runs when that inner block ends, before the program reaches D.

This is important. defer does not always wait until the function ends. It waits until the scope where it was declared ends.

Why defer Exists

Without defer, cleanup code is easy to forget.

Suppose you allocate memory:

const buffer = try allocator.alloc(u8, 1024);

Later, you need to free it:

allocator.free(buffer);

The problem is that the function may have many exits.

fn work(allocator: std.mem.Allocator) !void {
    const buffer = try allocator.alloc(u8, 1024);

    if (someCondition()) {
        allocator.free(buffer);
        return;
    }

    if (try somethingFails()) {
        allocator.free(buffer);
        return;
    }

    allocator.free(buffer);
}

This style is fragile. Every exit path must remember cleanup.

With defer, cleanup is placed next to the resource acquisition:

fn work(allocator: std.mem.Allocator) !void {
    const buffer = try allocator.alloc(u8, 1024);
    defer allocator.free(buffer);

    // use buffer
}

Now buffer is freed when the function scope ends.

This is cleaner because the allocation and cleanup are close together.

defer Runs on Early Return

defer still runs when a function returns early.

const std = @import("std");

fn example(x: u8) void {
    defer std.debug.print("cleanup\n", .{});

    if (x == 0) {
        std.debug.print("early return\n", .{});
        return;
    }

    std.debug.print("normal return\n", .{});
}

pub fn main() void {
    example(0);
}

This prints:

early return
cleanup

The function returns early, but the deferred statement still runs.

That is the main reason defer is useful. It protects cleanup from early exits.

Multiple defer Statements

You can have more than one defer.

They run in reverse order.

const std = @import("std");

pub fn main() void {
    defer std.debug.print("one\n", .{});
    defer std.debug.print("two\n", .{});
    defer std.debug.print("three\n", .{});
}

This prints:

three
two
one

The last deferred statement runs first.

This is deliberate. Cleanup usually needs to happen in the opposite order of setup.

Example:

setupA();
setupB();
setupC();

cleanupC();
cleanupB();
cleanupA();

defer naturally matches this pattern:

setupA();
defer cleanupA();

setupB();
defer cleanupB();

setupC();
defer cleanupC();

When the scope ends, cleanup happens as:

cleanupC
cleanupB
cleanupA

Practical Example: Freeing Memory

Here is a common pattern:

const std = @import("std");

fn makeMessage(allocator: std.mem.Allocator) !void {
    const message = try allocator.dupe(u8, "hello");
    defer allocator.free(message);

    std.debug.print("{s}\n", .{message});
}

pub fn main() !void {
    var gpa = std.heap.DebugAllocator(.{}){};
    defer _ = gpa.deinit();

    const allocator = gpa.allocator();

    try makeMessage(allocator);
}

The important part is:

const message = try allocator.dupe(u8, "hello");
defer allocator.free(message);

The memory is allocated, then cleanup is registered immediately.

When makeMessage ends, the memory is freed.

Practical Example: Closing a File

Files also need cleanup.

const std = @import("std");

pub fn main() !void {
    const file = try std.fs.cwd().openFile("notes.txt", .{});
    defer file.close();

    // use file here
}

The file is opened here:

const file = try std.fs.cwd().openFile("notes.txt", .{});

The file is closed here:

defer file.close();

Even if later code returns early with an error, the file is still closed.

defer Captures Variables by Reference

A deferred statement uses the variable when the defer runs, not when it is declared.

const std = @import("std");

pub fn main() void {
    var x: u8 = 1;

    defer std.debug.print("x = {}\n", .{x});

    x = 2;
}

This prints:

x = 2

The defer runs at the end of the scope, after x has changed.

This matters when deferred code depends on mutable variables.

If you need to preserve the current value, copy it into a constant first:

const std = @import("std");

pub fn main() void {
    var x: u8 = 1;
    const saved = x;

    defer std.debug.print("saved = {}\n", .{saved});

    x = 2;
}

This prints:

saved = 1

defer with Blocks

You can defer a block.

const std = @import("std");

pub fn main() void {
    defer {
        std.debug.print("cleanup step 1\n", .{});
        std.debug.print("cleanup step 2\n", .{});
    }

    std.debug.print("working\n", .{});
}

This prints:

working
cleanup step 1
cleanup step 2

Use a deferred block when cleanup needs several statements.

defer Is Not an Error Handler

defer always runs when the scope exits.

It does not care whether the scope exits successfully or because of an error.

fn work() !void {
    defer cleanup();

    try step1();
    try step2();
}

If step1 fails, cleanup() runs.

If step2 fails, cleanup() runs.

If both steps succeed, cleanup() still runs.

This is useful for resources that must always be released.

For cleanup that should happen only on error, Zig has errdefer. That belongs to the error handling chapter, but here is the basic idea:

const resource = try createResource();
errdefer destroyResource(resource);

errdefer runs only if the scope exits with an error.

defer runs on every exit.

defer and Loops

A defer inside a loop runs at the end of the scope where it is declared.

If the loop body is the scope, the deferred statement runs at the end of each iteration.

const std = @import("std");

pub fn main() void {
    for (0..3) |i| {
        defer std.debug.print("end iteration {}\n", .{i});
        std.debug.print("start iteration {}\n", .{i});
    }
}

This prints:

start iteration 0
end iteration 0
start iteration 1
end iteration 1
start iteration 2
end iteration 2

Each loop iteration has its own block scope.

This is useful when each iteration acquires a temporary resource.

A Common Mistake

Do not put defer in a long loop if you expect cleanup to happen immediately but the scope does not end soon.

This is usually fine:

for (files) |path| {
    const file = try open(path);
    defer file.close();

    // use file
}

The loop body scope ends each iteration, so the file closes each iteration.

But if you put defer in an outer function scope while repeatedly allocating, cleanup may happen much later than intended.

The rule is simple: know which scope owns the defer.

Cleanup Should Be Close to Setup

This is a good style:

const buffer = try allocator.alloc(u8, 4096);
defer allocator.free(buffer);

The cleanup is directly below the allocation.

This is less clear:

const buffer = try allocator.alloc(u8, 4096);

// many lines later

defer allocator.free(buffer);

A reader should not need to search for cleanup.

Put defer immediately after the operation that creates the responsibility.

The Main Idea

defer schedules cleanup for the end of the current scope.

It runs on normal exit, early return, and error return.

Multiple deferred statements run in reverse order.

Use it when something must be undone:

const buffer = try allocator.alloc(u8, 1024);
defer allocator.free(buffer);
const file = try std.fs.cwd().openFile("notes.txt", .{});
defer file.close();

The beginner rule is simple: when you acquire a resource, write its cleanup immediately after it.