Skip to content

`try`

try is a shortcut for a common operation:

try

try is a shortcut for a common operation:

Call a function that may fail. If it succeeds, use the value. If it fails, return the error from the current function.

This function may fail:

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

    return c - '0';
}

The caller can use try:

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

    return x * 10 + y;
}

This line:

const x = try parseDigit(a);

means roughly:

const x = parseDigit(a) catch |err| return err;

So try does not handle the error. It passes the error to the caller.

The current function must be able to return that error:

fn parseTwoDigits(a: u8, b: u8) !u8

The !u8 return type says the function may return an error.

Here is the full program:

const std = @import("std");

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

    return c - '0';
}

fn parseTwoDigits(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 parseTwoDigits('4', '2');
    std.debug.print("{d}\n", .{n});
}

The output is:

42

Now change the call:

const n = try parseTwoDigits('4', 'x');

The first digit succeeds. The second digit fails. The error returned by parseDigit is returned by parseTwoDigits, then returned by main.

try makes this chain explicit in the type, but compact in the code.

Without try, the same function can be written with catch:

fn parseTwoDigits(a: u8, b: u8) !u8 {
    const x = parseDigit(a) catch |err| return err;
    const y = parseDigit(b) catch |err| return err;

    return x * 10 + y;
}

This is longer and usually no clearer.

Use try when the current function cannot do anything useful with the error.

Use catch when the current function can handle, replace, log, or recover from the error.

For example:

fn parseDigitOrZero(c: u8) u8 {
    return parseDigit(c) catch 0;
}

This function handles the error by returning a default value.

The return type is plain u8, not !u8, because no error escapes.

try works with any error union type:

const file = try std.fs.cwd().openFile("data.txt", .{});
defer file.close();

If the file opens, file receives the opened file. If opening fails, the error is returned.

This pattern appears often:

const value = try operationThatMayFail();

Read it as:

get the value, or leave this function with the error.

try can also be used inside expressions:

const result = (try parseDigit('7')) + 1;

The value unwrapped by try is a u8, so it can be used in arithmetic.

But keep such expressions simple. Error flow should be easy to see.

The main rule is this: try propagates. It does not recover.

Exercise 8-13. Rewrite a function that uses catch |err| return err so that it uses try.

Exercise 8-14. Write parseThreeDigits using try.

Exercise 8-15. Write parseDigitOrZero using catch.

Exercise 8-16. Open a file with try, then close it with defer.