Idiom for an "old-school" try-catch block

Edited - added third pattern.

Three patterns for ya… first two are breaking blocks:

In this example, we break the value back to the result if it’s an error or a value. We can inspect or use it either way.

pub fn main() !void {

    const result = blk: {
        // break will return an error here
        const out = bar(42, 42) catch |e| break :blk e;

        // break will return the result
        break :blk out;
    };

    // unpack the error (can use if/else and switch for this too)
    const out = result catch |e| {
        return std.debug.print("caught an error: {}", .{e});
    };

    std.debug.print("Out: {}", .{ out });
}

The error can also be discarded with a raw break.

pub fn main() !void {

    blk: {
        const out = foo(42, 42) catch break :blk;
        // the happy path...
        std.log.info("Result: {}", .{out});        
    }
    // the not-so-happy path...
    std.log.info("Something bad happened.", .{});
}

This is closer to the canonical try/catch that you’re talking about. You break the block in the same way you’d use a throw. The obvious issue with the second one is you lose whatever error you had. It’s really up to you if you care about that or not.

Another pattern to consider is try/catch |...| return ... and catch |...| switch (...):

const MyError = error { A, B, C };

pub fn foo(_: i32, _: i32) MyError!i32 {
    return MyError.A;
}

pub fn bar(_: i32, _: i32) MyError!i32 {
    return MyError.B;
}
pub fn baz(x: i32, y: i32) MyError!i32 {
    // try works as a short hand here
    const u = try foo(x, y);
    // more verbose but can be further customized
    const v = bar(u, x) catch |e| return e;
    return v;
}

pub fn main() !void {

    const out: i32 = baz(42, 42) catch |e| switch (e) {
        MyError.A => return std.log.err("Handle Error A.", .{}),
        MyError.B => return std.log.err("Handle Error B.", .{}),
        MyError.C => return std.log.err("Handle Error C.", .{}),
    };

    std.log.info("Result: {}", .{out});
}

This pattern forces the error to go upstream to the call site so you will end up doing your error handling in one place. You’ll notice in the switch block, I’m doing returns because out will be considered void and we have to evacuate the function if an error occurs.

We had a long discussion about these: What is the best way to handle unrecoverable errors like OutOfMemory in an application?

And that was based on this thread: Zig Code Smells

1 Like