Zig Errdefer Patterns
An errdefer is a cleanup step that runs only when its function returns an error. It keeps failed paths tidy without touching successful ones.
Cleanup only on failure
On success nothing is cleaned up. On failure the errdefer runs before the caller handles the error.
const std = @import("std");
const WorkError = error{ Boom };
fn log(msg: []const u8) void {
const out = std.io.getStdOut().writer();
out.print("{s}\n", .{msg}) catch {};
}
fn work(fail: bool) WorkError!i32 {
errdefer log("cleaned up");
if (fail) return WorkError.Boom;
return 10;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = work(false) catch 0;
try out.print("success: {d}\n", .{a});
const b = work(true) catch 0;
try out.print("failed: {d}\n", .{b});
}
Reverse order
Two errdefers run in reverse order of declaration.
const std = @import("std");
const WorkError = error{ Boom };
fn log(msg: []const u8) void {
const out = std.io.getStdOut().writer();
out.print("{s}\n", .{msg}) catch {};
}
fn work(fail: bool) WorkError!i32 {
errdefer log("cleanup A");
errdefer log("cleanup B");
if (fail) return WorkError.Boom;
return 10;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = work(true) catch 0;
try out.print("result: {d}\n", .{a});
}
Inner before outer
Inner cleanup runs before outer cleanup as the error travels outward.
const std = @import("std");
const WorkError = error{ Boom };
fn log(msg: []const u8) void {
const out = std.io.getStdOut().writer();
out.print("{s}\n", .{msg}) catch {};
}
fn inner(fail: bool) WorkError!i32 {
errdefer log("inner cleanup");
if (fail) return WorkError.Boom;
return 5;
}
fn outer(fail: bool) WorkError!i32 {
errdefer log("outer cleanup");
const v = try inner(fail);
return v + 1;
}
pub fn main() !void {
const out = std.io.getStdOut().writer();
const a = outer(true) catch 0;
try out.print("result: {d}\n", .{a});
}
Example
const std = @import("std");
const WorkError = error{ Boom };
fn log(msg: []const u8) void {
const out = std.io.getStdOut().writer();
out.print("{s}\n", .{msg}) catch {};
}
fn load(alloc: std.mem.Allocator, fail: bool) (WorkError || error{OutOfMemory})![]u8 {
const buf = try alloc.alloc(u8, 3);
errdefer {
alloc.free(buf);
log("freed on failure");
}
if (fail) return WorkError.Boom;
buf[0] = 'a';
buf[1] = 'b';
buf[2] = 'c';
return buf;
}
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const alloc = gpa.allocator();
const out = std.io.getStdOut().writer();
if (load(alloc, false)) |buf| {
defer alloc.free(buf);
try out.print("loaded: {s}\n", .{buf});
} else |err| {
try out.print("failed: {s}\n", .{@errorName(err)});
}
if (load(alloc, true)) |buf| {
defer alloc.free(buf);
try out.print("loaded: {s}\n", .{buf});
} else |err| {
try out.print("failed: {s}\n", .{@errorName(err)});
}
}
Key points
- Errdefer: an errdefer is a cleanup step that runs only when its function returns an error.
- Defer runs on every exit, while errdefer runs only on the error exit, so the two fit different cleanups.
- Multiple errdefers run last in, first out, so the most recent cleanup runs first.
- When an error passes through several functions, the innermost cleanup runs first, then each outer cleanup in turn.
- Errdefer fits resources a function owns only until success, like a buffer that the caller takes over on success.
- An errdefer runs before the error reaches the caller, so the caller never sees a half-cleaned result.
load allocates a buffer and registers its release as an errdefer. On success the caller owns the buffer and frees it. On failure the errdefer frees the buffer first, so no buffer leaks and the caller only sees the error.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
A function has errdefer A, then errdefer B, and then returns an error. Which cleanup runs first?