Skip to content

`catch`

catch handles an error at the place where it happens.

catch

catch handles an error at the place where it happens.

Use try when you want to pass the error to the caller.

Use catch when you want to decide what to do right now.

A function call that returns an error union has two possible outcomes:

success: use the value
failure: handle the error

catch gives you the failure branch.

Basic catch

Suppose we have this function:

const ParseError = error{
    InvalidDigit,
};

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

    return c - '0';
}

The return type is:

ParseError!u8

So the call may return either an error or a u8.

With catch, we can provide a fallback value:

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

Read it as:

try to parse the digit;
if parsing fails, use 0

So digit becomes 0.

After this line, digit is a plain u8, not an error union.

catch Must Return the Same Success Type

This matters.

The success value from parseDigit is a u8.

So the fallback after catch must also produce a u8.

This works:

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

This does not make sense:

const digit = parseDigit('x') catch "failed";

The success side gives a number. The error side gives text. Zig needs one final type for digit.

Both paths must fit the same result type.

Capturing the Error

Sometimes you need to know which error happened.

Use catch |err|:

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

Inside the block, err is the error value.

For this example, err can only be:

error.InvalidDigit

In larger functions, the error set may contain many possible errors.

Handling Errors with switch

A common pattern is catch plus switch.

const digit = parseDigit('x') catch |err| switch (err) {
    error.InvalidDigit => 0,
};

Read it as:

if parsing succeeds, use the parsed digit;
if parsing fails with InvalidDigit, use 0

This becomes more useful when there are multiple errors.

const ParseError = error{
    EmptyInput,
    InvalidDigit,
    TooLong,
};

fn parseOneDigit(text: []const u8) ParseError!u8 {
    if (text.len == 0) {
        return error.EmptyInput;
    }

    if (text.len > 1) {
        return error.TooLong;
    }

    const c = text[0];

    if (c < '0' or c > '9') {
        return error.InvalidDigit;
    }

    return c - '0';
}

Now handle each error differently:

const digit = parseOneDigit(input) catch |err| switch (err) {
    error.EmptyInput => 0,
    error.InvalidDigit => 0,
    error.TooLong => 9,
};

This gives the program a clear local policy.

Empty input becomes 0.

Invalid input becomes 0.

Too-long input becomes 9.

This is only an example. In a real parser, you might return the error upward instead.

catch return

Sometimes you want to inspect the error, then return it.

const value = parseOneDigit(input) catch |err| {
    std.debug.print("failed: {}\n", .{err});
    return err;
};

This is similar to try, but with extra work before returning.

Use this when you need to log, clean up, or convert the error.

Converting One Error into Another

A lower-level function may return errors that do not belong in your public API.

You can use catch to map them into your own error set.

const AppError = error{
    BadInput,
};

fn readNumber(text: []const u8) AppError!u8 {
    return parseOneDigit(text) catch |err| switch (err) {
        error.EmptyInput => error.BadInput,
        error.InvalidDigit => error.BadInput,
        error.TooLong => error.BadInput,
    };
}

Here, callers of readNumber only see:

error.BadInput

They do not need to know the internal details of parseOneDigit.

This is useful for clean API boundaries.

catch unreachable

Sometimes an error is impossible because you already checked the input.

Example:

fn digitAfterCheck(c: u8) u8 {
    if (c < '0' or c > '9') {
        return 0;
    }

    return parseDigit(c) catch unreachable;
}

We checked that c is a digit before calling parseDigit.

So parseDigit(c) should not fail.

catch unreachable says:

if this error happens, the program has reached an impossible state

Use this carefully. It is a claim to the compiler and to future readers. If your claim is wrong, your program has a bug.

Do not use catch unreachable just to silence errors.

catch with a Block

The right side of catch can be a block.

const digit = parseOneDigit(input) catch |err| blk: {
    std.debug.print("parse error: {}\n", .{err});
    break :blk 0;
};

The block returns a value using break :blk.

Here, the fallback value is 0.

This style is useful when handling the error needs several lines, but still produces a value.

For very simple cases, this is easier:

const digit = parseOneDigit(input) catch 0;

catch and void

For a function that returns !void, there is no success value.

fn saveConfig() !void {
    // may fail
}

You can handle failure like this:

saveConfig() catch |err| {
    std.debug.print("could not save config: {}\n", .{err});
    return;
};

If saveConfig succeeds, execution continues.

If it fails, the block runs.

catch vs try

These two lines look similar, but they mean different things.

const value = try parseOneDigit(input);

This means:

if parsing fails, return the error to my caller

This line handles the error locally:

const value = parseOneDigit(input) catch 0;

This means:

if parsing fails, use 0 and keep going

Use try when the current function does not know what to do.

Use catch when the current function does know what to do.

Avoid Hiding Real Errors

A fallback can be useful, but it can also hide bugs.

This is sometimes fine:

const port = parsePort(text) catch 8080;

A default port may be reasonable.

This is dangerous:

const user_id = parseUserId(text) catch 0;

If 0 is a real user ID or a special admin ID, this fallback could cause serious mistakes.

A good fallback must be safe and intentional.

When in doubt, propagate the error with try.

The Core Idea

catch handles an error immediately.

It lets you write:

const value = operation() catch fallback;

or:

const value = operation() catch |err| {
    // inspect or handle err
};

After catch, you have either the original success value or the fallback value.

That is the role of catch: turn an error union into a normal result by deciding what failure means at this point in the program.