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 0The 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.