zig-patron-defer

defer in Zig: Resource Cleanup on Block Exit

  • 4 min

The defer statement in Zig is a way to automatically execute code when exiting the current block. When working with systems languages, resource management is critical: if you open a file, you must close it; if you request memory, you must free it.

In languages like C, cleanup code often ends up far from the resource acquisition, usually at the end of the function. With multiple exit points, it’s easy to miss a path and cause a leak.

Zig introduces the defer statement for this purpose.

The idea is simple: “execute this code when the flow exits the current block”. This includes exits via return, break, or error propagation, but it does not guarantee cleanup in case of an abrupt process termination.

Basic Syntax

We use the keyword defer followed by an expression or a block of code. Zig schedules that execution for the exact moment the execution flow leaves the current {}.

pub fn example() void {
    std.debug.print("Start\n", .{});
    
    // This will execute AT THE END of the function
    defer std.debug.print("End (executed by defer)\n", .{});
    
    std.debug.print("Doing stuff...\n", .{});
}
Copied!

Output:

Start
Doing stuff...
End (executed by defer)
Copied!

This way we can place cleanup immediately after acquiring the resource. Visually, the “open” and “close” remain together, making the code easier to review.

Order Matters: LIFO

What happens if we have multiple defer statements in the same block? Zig executes them in reverse order of their declaration (Last In, First Out - LIFO).

This makes perfect sense if we think about dependencies. If you create A and then create B (which depends on A), you need to destroy B before destroying A.

{
    defer std.debug.print("1. Cleaning A\n", .{});
    defer std.debug.print("2. Cleaning B\n", .{});
    defer std.debug.print("3. Cleaning C\n", .{});
    
    std.debug.print("Working...\n", .{});
}
Copied!

Output:

Working...
3. Cleaning C
2. Cleaning B
1. Cleaning A
Copied!

defer and Error Handling

The clearest benefit of defer appears when we start handling errors and early returns.

Suppose a function has to perform three steps. If step 2 fails, step 1 must be cleaned up. If step 3 fails, steps 1 and 2 must be cleaned up. In C, this is a nightmare of goto or nested if statements.

In Zig, it’s linear and clean:

fn processData() !void {
    const resource1 = createResource1();
    defer destroy(resource1); // Will be cleaned up no matter what

    // If this fails and returns an error, resource1 is cleaned automatically
    try dangerousStep(); 

    const resource2 = createResource2();
    defer destroy(resource2); // Now 2 and then 1 will be cleaned

    try anotherDangerousStep();
}
Copied!

If anotherDangerousStep returns an error, Zig will execute destroy(resource2) and then destroy(resource1) before propagating it to the caller.

Difference from Go: Block Scope

If you come from Go, keep this difference in mind:

  • In Go, defer executes at the end of the function.
  • In Zig, defer executes at the end of the block (scope).

This allows precise control over resources acquired within loops or inner blocks.

// Example: Defer inside a loop
for (files) |file| {
    const handle = open(file);
    // In Zig, this closes the file AT THE END OF EACH ITERATION
    // In Go, files would remain open until exiting the function (dangerous)
    defer close(handle);
    
    process(handle);
}
Copied!

Thanks to block scope, we don’t exhaust file descriptors or memory in long loops.

Common Use Cases

Memory Management

This is the most common use. You request memory and immediately place the defer.

const memory = try allocator.alloc(u8, 100);
defer allocator.free(memory);
Copied!

Files and Sockets

const file = try dir.openFile("data.txt", .{});
defer file.close();
Copied!

Mutex and Concurrency

Prevents forgetting to unlock a semaphore and causing a deadlock.

mutex.lock();
defer mutex.unlock();

// Do whatever you want, even return with an error; the mutex will be released.
doCriticalStuff();
Copied!

Resource Leaks

Although defer helps enormously, it does not do it alone. It only cleans what you tell it to clean.

If you have a complex data structure (like a tree or a linked list) and only do defer allocator.free(root), you will only free the root node. You are responsible for writing a deinit() function that traverses and cleans everything, and then calling defer tree.deinit().

Many types in the standard library have a deinit method. Its signature depends on the type and may require the allocator, for example defer list.deinit(allocator).