Zig defer Patterns

A short note about some unexpected usages of Zigs defer statement.

This post assumes that you already know the basics about RAII, defer and errdefer. While discussing the differences between them is not the point, I will allow myself one high level comment. I dont like defer as a replacement for RAII: after writing Zig for some time, I am relatively confident that humans are just not good at not forgetting defers, especially whenoptional ownership transfer is at play (i.e, this function takes ownership of an argument, unless an error is returned). But defer is good at discouraging RAII oriented programming. RAII encourages binding lifetime of resources (such as memory) with lifetimes of individual domain objects (such as a String). But often, in pursuit of performance and small code size, you want to separate the two concerns, and let many domain objects to share the single pool of resources. Instead of each individual string managing its own allocation, you might want to store the contents of all related strings into a single continuously allocated buffer. Because RAII with defer is painful, Zig naturally pushes you towards batching your resource acquisition and release calls, such that you have far fewer resources than objects in your program.

But, as Ive said, this post isnt about all that. This post is about non-resource-oriented usages of defer. Theres more to defer than just RAII, its a nice little powerful construct! This is way to much ado already, so here come the patterns:

Asserting Post Conditions

defer gives you poor mans contract programming in the form of

assert(precondition)
defer assert(postcondition)

Real life example:

{
  assert(!grid.free_set.opened);
  defer assert(grid.free_set.opened);

  // Code to open the free set
}

Statically Enforcing Absence of Errors

This is basically peak Zig:

errdefer comptime unreachable

errdefer runs when a function returns an error (e.g., when a try fails). unreachable crashes the program (in ReleaseSafe). But comptime unreachable straight up fails compilation if the compiler tries to generate the corresponding runtime code. The three together ensure the absence of error-returning paths.

Heres an example from the standard library, the function to grow a hash map:

// The function as a whole can fail...
fn grow(
  self: *Self,
  allocator: Allocator,
  new_capacity: Size,
) Allocator.Error!void {
  @setCold(true);
  var map: Self = .{};
  try map.allocate(allocator, new_capacity);

  // ...but from this point on, failure is impossible
  errdefer comptime unreachable;

  // Code to rehash&copy self to map
  std.mem.swap(Self, self, &map);
  map.deinit(allocator);
}

Logging Errors

Zigs error handling mechanism provides only error code (a number) and an error trace. This is usually plenty to programmatically handle the error in an application and for the operator to debug a failure, but this is decidedly not enough to provide a nice report for the end user. However, if you are in a business of reporting errors to users, you are likely writing an application, and application might get away without propagating extra information about the error to the caller. Often, theres enough context at the point where the error originates in the first place to produce a user-facing report right there.

Example:

const port = port: {
  errdefer |err| log.err("failed to read the port number: {}", .{err});

  var buf: [fmt.count("{}\n", .{maxInt(u16)})]u8 = undefined;
  const len = try process.stdout.?.readAll(&buf);
  break :port try fmt.parseInt(u16, buf[0 .. len -| 1], 10);
};

Post Increment

Finally, defer can be used as an i++ of sorts. For example, heres how you can pop an item off a free list:

pub fn acquire(self: *ScanBufferPool) Error!*const ScanBuffer {
  if (self.scan_buffer_used == constants.lsm_scans_max) {
    return Error.ScansMaxExceeded;
  }

  defer self.scan_buffer_used += 1;
  return &self.scan_buffers[self.scan_buffer_used];
}