Skip to content

Error Unions

An error union combines a normal value with an error set.

An error union combines a normal value with an error set.

The type:

ReadError!u32

means:

either a u32, or an error from ReadError.

This is the fundamental error-handling type in Zig.

A function that cannot fail returns a normal type:

fn square(x: u32) u32 {
    return x * x;
}

A function that may fail returns an error union:

fn parseDigit(c: u8) !u8 {
    if (c < '0' or c > '9') {
        return error.InvalidDigit;
    }

    return c - '0';
}

The caller must consider both outcomes.

The returned value cannot be used directly:

const n = parseDigit('7');

because n is not a u8.

Its type is:

!u8

The value must first be unwrapped.

The most common way is try:

const n = try parseDigit('7');

If the function succeeds, n receives the u8.

If the function fails, the error is returned from the current function.

This produces direct and compact code:

const std = @import("std");

fn parseDigit(c: u8) !u8 {
    if (c < '0' or c > '9') {
        return error.InvalidDigit;
    }

    return c - '0';
}

fn parsePair(a: u8, b: u8) !u8 {
    const x = try parseDigit(a);
    const y = try parseDigit(b);

    return x * 10 + y;
}

pub fn main() !void {
    const n = try parsePair('4', '2');
    std.debug.print("{d}\n", .{n});
}

The flow is straightforward:

  1. call parseDigit
  2. if it fails, return the error
  3. otherwise continue

No hidden control flow is involved.

The error union may also be handled explicitly with catch.

const n = parseDigit('x') catch 0;

If parsing fails, the expression becomes 0.

This is similar to:

const n = if (parse succeeds) value else 0;

A larger example:

const std = @import("std");

fn parseDigit(c: u8) !u8 {
    if (c < '0' or c > '9') {
        return error.InvalidDigit;
    }

    return c - '0';
}

pub fn main() void {
    const a = parseDigit('8') catch 0;
    const b = parseDigit('x') catch 0;

    std.debug.print("{d} {d}\n", .{ a, b });
}

The output is:

8 0

catch may also introduce a named error value:

const n = parseDigit('x') catch |err| {
    std.debug.print("error: {}\n", .{err});
    return;
};

The variable err holds the error value.

Error unions work naturally with control flow because they are expressions.

This function:

fn maybeRead(ok: bool) !u32 {
    if (!ok) {
        return error.ReadFailed;
    }

    return 123;
}

returns one of two possibilities:

ResultMeaning
123success
error.ReadFailedfailure

The compiler ensures that both cases are considered.

An error union occupies enough space to store either form. Zig handles the representation automatically.

Error unions are used throughout the standard library. File operations, allocation, parsing, networking, formatting, and process control all return error unions.

This keeps failure visible in the type system.

Exercise 8-5. Write a function that parses a lowercase letter and returns error.InvalidLetter for any other byte.

Exercise 8-6. Write a function that returns the first byte of a slice or error.EmptySlice.

Exercise 8-7. Rewrite a program from an earlier chapter so that it uses try.

Exercise 8-8. Write a function that divides two integers and uses catch to return zero on division failure.