The errdefer statement is a variant of defer that cleans up when propagating an error from its scope. In the previous article we saw that defer acts on any normal exit from the block; now we will see how to reserve cleanup for error paths.
What happens when we are building a complex object and want to transfer it to the caller only if it is complete?
Suppose a createCar() function needs to do two things:
- Buy the engine (reserve memory).
- Paint the bodywork (an operation that can fail).
If we use defer destroy(engine), the engine will always be destroyed at the end of the function, even if everything went well! And if we do nothing, and step 2 fails, we are left with a “dangling” engine (memory leak) because we never reached the return.
That’s why Zig gives us errdefer.
The concept of rollback
errdefer tells the compiler: “Execute this cleanup line ONLY if the function finishes by returning an error”.
If the function succeeds and returns a normal value (with a clean return), the errdefer is ignored.
This allows us to apply transactional semantics to memory management: either we create the complete object or we undo the partial reservations.
Practical example: multi-step construction
Let’s look at the classic case of multiple memory allocations. We want to create a Player that has a name (dynamic string) and an inventory (dynamic array).
const Player = struct {
name: []u8,
inventory: []u8,
};
fn createPlayer(allocator: Allocator) !*Player {
// 1. Reserve memory for the base struct
const player = try allocator.create(Player);
// If we fail later, free the player struct
errdefer allocator.destroy(player);
// 2. Reserve memory for the name
// If this fails, the previous errdefer triggers and frees 'player'
player.name = try allocator.alloc(u8, 10);
// Now we have TWO things to clean up if the next step fails.
// We add another errdefer for the name.
errdefer allocator.free(player.name);
// 3. Reserve memory for the inventory (final dangerous step)
// If this fails, both errdefers execute in reverse order (LIFO)
player.inventory = try allocator.alloc(u8, 100);
// 4. SUCCESS!
// We reach the end. We return the pointer.
// The 'errdefer's are DEACTIVATED and do not execute.
return player;
}Let’s analyze the flow:
- If step 1 fails (
create): The function returns error immediately. Noerrdefers are active yet. - If step 2 fails (
alloc name):destroy(player)executes and the error is returned. - If step 3 fails (
alloc inventory):free(name)executes, thendestroy(player)executes, and the error is returned. - If everything goes well: No cleanup is executed. The caller receives a complete object and now it is their responsibility to clean it up (probably with a
destroyPlayerfunction).
Key difference with defer
It is important not to confuse them: choosing the wrong one can free a valid result or leave a reservation unfreed.
| Statement | Execution condition | Typical usage |
|---|---|---|
defer | When exiting the block via normal flow, including return or try. | Closing files, unlocking mutexes, freeing local temporary memory. |
errdefer | When propagating an error while still active in its scope. | Undoing partial changes to objects we intend to return. |
Combining them
It is very common to see functions that use both. defer for temporary variables that only serve within the function, and errdefer for building the result.
fn readConfig(allocator: Allocator) !*Config {
// We open a temporary file (we need to close it always when the function ends)
var file = try openFile("temp.txt");
defer file.close();
// Reserve memory for the config (we want to return it, not delete it if successful)
const config = try allocator.create(Config);
errdefer allocator.destroy(config);
// Read from the file and fill the config
try fillConfig(file, config);
return config;
}errdefer in blocks
Like defer, errdefer is tied to the scope where it is declared.
It only becomes active after execution passes through its declaration. If the scope ends normally, it is deactivated; if an error is propagated from it, the cleanup executes before leaving it.
An errdefer declared inside the body of a for belongs to that iteration. If the iteration ends normally it is deactivated; if it propagates an error, it cleans up the resources acquired during that turn.
The init and deinit pattern
The use of errdefer fits a common pattern in Zig: init and deinit.
Instead of having constructors that throw exceptions halfway through, in Zig we usually write:
init(): Creates the object. Useserrdeferinternally to ensure that if it fails, it returns nothing dirty.deinit(): Cleans up the entire object.
This guarantees that, from the outside, the object has two possible states: fully valid or fully non-existent. Never a “zombie” state halfway initialized.