Defer
const std = @import("std");
const expect = std.testing.expect;
test "defer runs at scope exit" {
var x: i16 = 5;
{
defer x += 2;
try expect(x == 5); // not yet
}
try expect(x == 7); // now
}
test "defers run in reverse order" {
// Last registered runs first, so cleanup unwinds in the order things
// were acquired.
var order: [3]u8 = undefined;
var index: usize = 0;
{
defer {
order[index] = 1;
index += 1;
}
defer {
order[index] = 2;
index += 1;
}
defer {
order[index] = 3;
index += 1;
}
}
try expect(order[0] == 3);
try expect(order[2] == 1);
}
fn mightFail(fail: bool) !u8 {
var cleaned = false;
// `errdefer` runs only when the function returns an error.
errdefer cleaned = true;
if (fail) return error.Nope;
return @intFromBool(cleaned);
}
test "errdefer only fires on the error path" {
try expect(try mightFail(false) == 0);
try std.testing.expectError(error.Nope, mightFail(true));
}defer schedules an expression to run when the enclosing scope exits: not
the function, the scope. Every path counts: falling off the end, an early
return, or an error propagating through try.
Reverse order is the point
Deferred statements run last-registered-first. That is what makes them compose: if you acquire A then B, cleanup happens B then A, which is almost always the correct unwinding order. Writing the release directly under the acquire keeps the pair visible in one glance:
const buf = try allocator.alloc(u8, n);
defer allocator.free(buf);
errdefer for the failure path only
errdefer runs only when the function returns an error. This is how you
write a constructor that cleans up partial work without also undoing itself on
success:
const thing = try create();
errdefer destroy(thing); // only if a later step fails
try initialise(thing);
return thing;
With plain defer that would destroy the object you just successfully
returned.