Skip to content

`errdefer`

errdefer is a cleanup tool.

errdefer

errdefer is a cleanup tool.

It means:

run this cleanup code only if the function returns with an error

It is similar to defer, but more specific.

defer runs when the function exits for any reason.

errdefer runs only when the function exits because of an error.

The Problem errdefer Solves

Many functions build something step by step.

For example, a function might:

allocate memory
open a file
initialize a resource
do more work
return the finished result

If every step succeeds, the caller receives the finished result.

But if one step fails halfway through, the function must clean up the work it already did.

That is where errdefer is useful.

A Simple Allocation Example

Suppose we allocate a buffer and return it:

const std = @import("std");

fn makeBuffer(allocator: std.mem.Allocator) ![]u8 {
    const buffer = try allocator.alloc(u8, 1024);
    errdefer allocator.free(buffer);

    // more work could fail here

    return buffer;
}

This line allocates memory:

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

If allocation fails, the function returns immediately. No cleanup is needed, because no buffer was created.

This line registers error cleanup:

errdefer allocator.free(buffer);

Now Zig knows:

if this function later returns an error, free buffer

If the function succeeds, errdefer does not run. That is correct, because the function returns buffer to the caller. The caller now owns it.

Why defer Would Be Wrong Here

You might think we should write:

defer allocator.free(buffer);

But that would free the buffer even when the function succeeds.

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

    return buffer;
}

This is wrong.

The function returns a slice pointing to memory that has already been freed. The caller receives a dangling slice.

Use defer when the current function should always clean up.

Use errdefer when the current function should clean up only if construction fails.

Building a Resource in Steps

Here is a more realistic example:

const std = @import("std");

const Resource = struct {
    name: []u8,
    data: []u8,
};

fn createResource(
    allocator: std.mem.Allocator,
    name: []const u8,
) !Resource {
    const owned_name = try allocator.dupe(u8, name);
    errdefer allocator.free(owned_name);

    const data = try allocator.alloc(u8, 4096);
    errdefer allocator.free(data);

    return Resource{
        .name = owned_name,
        .data = data,
    };
}

This function creates two owned pieces of memory.

First:

const owned_name = try allocator.dupe(u8, name);
errdefer allocator.free(owned_name);

If later work fails, owned_name must be freed.

Then:

const data = try allocator.alloc(u8, 4096);
errdefer allocator.free(data);

If later work fails, data must also be freed.

If both allocations succeed, the function returns a Resource. The caller owns both fields.

Cleanup Runs in Reverse Order

Like defer, multiple errdefer statements run in reverse order.

fn example(allocator: std.mem.Allocator) !void {
    const a = try allocator.alloc(u8, 10);
    errdefer allocator.free(a);

    const b = try allocator.alloc(u8, 20);
    errdefer allocator.free(b);

    return error.Failed;
}

When the function returns error.Failed, cleanup runs like this:

free b
free a

This is usually what you want.

Resources are cleaned up in the opposite order from how they were acquired.

errdefer and Ownership

errdefer is closely tied to ownership.

When a function is building a value to return, there is a period where the function owns the parts.

If construction fails, the function must destroy those parts.

If construction succeeds, ownership moves to the caller.

errdefer expresses exactly that pattern:

I own this for now.
If I fail, I clean it up.
If I succeed, I give it away.

This is one of the clearest uses of errdefer.

errdefer with Files

errdefer is not only for memory.

It works with any cleanup code.

fn createOutputFile(path: []const u8) !std.fs.File {
    const file = try std.fs.cwd().createFile(path, .{});
    errdefer file.close();

    // more setup could fail here

    return file;
}

If file creation succeeds but later setup fails, file.close() runs.

If the function succeeds, the file is returned to the caller. The caller must close it.

defer and errdefer Together

Sometimes you use both.

fn writeMessage(path: []const u8, message: []const u8) !void {
    const file = try std.fs.cwd().createFile(path, .{});
    defer file.close();

    try file.writeAll(message);
}

Here defer is correct. The function does not return the file. The current function owns the file for its whole lifetime and should always close it.

Now compare:

fn openMessageFile(path: []const u8) !std.fs.File {
    const file = try std.fs.cwd().openFile(path, .{});
    errdefer file.close();

    // validate file before returning it

    return file;
}

Here errdefer is correct. If validation fails, close the file. If validation succeeds, return the file to the caller.

errdefer Can Capture the Error

errdefer can also capture the error that caused the function to fail.

fn run() !void {
    errdefer |err| {
        std.debug.print("failed with error: {}\n", .{err});
    }

    return error.SomethingWentWrong;
}

If run returns an error, the errdefer block runs and receives that error.

This is useful for logging or diagnostics.

Use it carefully. Cleanup code should usually be simple. If error cleanup becomes large, consider moving it into a helper function.

errdefer Does Not Run on Success

This is the most important rule.

fn example() !void {
    errdefer std.debug.print("cleanup\n", .{});

    return;
}

The function succeeds. The errdefer does not run.

But here:

fn example() !void {
    errdefer std.debug.print("cleanup\n", .{});

    return error.Failed;
}

The function fails. The errdefer runs.

errdefer Does Not Replace Good Design

errdefer is useful, but it does not remove the need to design ownership clearly.

When writing a function, ask:

Who owns this value right now?

Who frees it if the next step fails?

Who owns it after the function returns successfully?

If the answer is “this function owns it until success,” errdefer is often the right tool.

A Common Beginner Mistake

A common mistake is using errdefer when defer is needed.

For example:

fn printFile(path: []const u8) !void {
    const file = try std.fs.cwd().openFile(path, .{});
    errdefer file.close();

    // read and print the file
}

This is probably wrong.

If the function succeeds, errdefer does not run, so the file stays open.

Since this function does not return the file to the caller, it should use defer:

fn printFile(path: []const u8) !void {
    const file = try std.fs.cwd().openFile(path, .{});
    defer file.close();

    // read and print the file
}

Use defer for cleanup that must always happen.

Use errdefer for cleanup that should happen only when success does not happen.

The Core Idea

errdefer means:

if this function fails later, run this cleanup

It is mainly used when building owned resources step by step.

If the function succeeds, ownership usually moves to the caller, so errdefer stays silent.

If the function fails, errdefer prevents leaks and half-built resources.