Skip to content

`catch`

catch handles an error union.

catch

catch handles an error union.

Where try passes an error to the caller, catch deals with it in the current expression.

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

If parseDigit('x') succeeds, n receives the digit. If it fails, n receives 0.

A complete 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

The right side of catch must produce the same final type as the successful value. Here the successful value is u8, and the fallback value 0 can be used as a u8.

catch may also bind the error:

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

The name err is available only inside the block after catch.

A common use is to translate one error into another:

fn readConfig() ![]const u8 {
    return readFile("config.txt") catch |err| switch (err) {
        error.FileNotFound => error.MissingConfig,
        else => err,
    };
}

This says: if the file is missing, return a program-level error. For all other failures, return the original error.

catch is an expression. It can return a value:

const value = parseDigit(c) catch 0;

or it can leave the function:

const value = parseDigit(c) catch |err| return err;

That second form is exactly what try abbreviates.

So this:

const value = try parseDigit(c);

means:

const value = parseDigit(c) catch |err| return err;

Use catch when the local code has a policy.

Examples of local policy:

const port = parsePort(text) catch 8080;
const file = openLogFile() catch {
    std.debug.print("log file unavailable\n", .{});
    return;
};
const user = findUser(id) catch |err| switch (err) {
    error.NotFound => return null,
    else => return err,
};

catch should not hide real failure by accident. A fallback value is good only when it is truly acceptable.

This is usually bad:

const bytes = readFile(path) catch "";

An empty file and a failed read are different facts. Code that treats them as the same will lose information.

Prefer this:

const bytes = readFile(path) catch |err| {
    std.debug.print("cannot read file: {}\n", .{err});
    return err;
};

or this:

const bytes = try readFile(path);

A catch block may contain several statements:

const n = parseDigit(c) catch |err| {
    std.debug.print("bad digit: {}\n", .{err});
    return error.BadInput;
};

The block returns error.BadInput, so the whole expression leaves the current function with that error.

catch is most useful at boundaries: command-line input, configuration loading, file access, network calls, and places where a program decides whether to recover, retry, substitute a default, or stop.

Inside ordinary helper functions, try is usually better. It keeps the error visible and lets a higher layer decide what the error means.

Exercise 8-17. Write parseDigitOrNine using catch.

Exercise 8-18. Write a function that converts error.FileNotFound into error.MissingInput.

Exercise 8-19. Write a program that tries to parse a digit and prints a message when parsing fails.

Exercise 8-20. Find a place where catch 0 would be wrong. Explain why.