Captures and Payloads

Captures in Zig provide the ability to unpack or catch values as an extension of other language features. Capture syntax provide an elegant way to reduce boilerplate and clearly express intent with a minimal syntax.

Data can be captured via a pointer or direct copy. To see common issues surrounding this, see: Unintentional Copy On Capture


With booleans (no captures here):

if (true) return x;
if (a > b) x = a else x = b;

With optionals (capture payload if not null):

if (maybe_byte) |byte| x = byte else x = 0;
if (maybe_byte == null) return;

With error unions (capture payload on success, or the error value on error):

if (canFail()) |result| {
    x = result;
} else |err| {
   print("error: {}\n", .{err});
   return err;


With booleans (no capture):

while (i < n) : (i += 1) { ... }

With optionals (capture the payload if not null; break loop if null):

while (maybe_byte) |byte| { ... }

With error unions (capture payload on success, else clause is mandatory here):

while (canFail()) |result| { ... } else |err| { ... }


for loops only work on arrays, slices, and ranges. The payload is always the current iteration item. You can iterate over multiple objects simultaneously.

for (list) |item| { ... }
for (list_a, 0..) |item, i| { ... }
for (list_a, list_b, list_c) |a, b, c| { ... }


When switching on a tagged union, the prongs can capture the payload if any. Also when a prong expression is a range or a list of expressions, you can capture the actual matching value.

In addition, when switching on a non-exhaustive enum you can match on _ prong instead of else, making sure at compile-time that the switch is exhaustive, i.e. uses all known enum tags.

switch (my_tagged_union) {
    .a => |a| ...,
    .b => ...,
    .c => |c| ...,

switch (x) {
    0...9 => |n| ...,
    13, 19, 23 => |n| ...,
    else => ...,

switch (my_non_exhaustive_enum) {
    .a, .b => |a_b| ...,
    .c => |c| ...,
    _ => ...,


The catch-capture works with error unions as a trailing syntax. Expressions that result in an error can be caught and the payload can be captured for further handling.

// Reserve memory for a type T. If the create statement succeeds
// the variable ptr will be of type *T. If it fails, the error
// returned by create is captured and can be further handled 
// in the catch block.

const ptr = allocator.create(T) catch |e| { 
    // handling code based on the captured error "e"


The errdefer-capture allows capturing the error to be used in the deferred block:

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