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

i’m finding that i will frequently suffix many library function calls with a catch unreachable or maybe a catch std.zig.fatal(...)

back in the day, i would often wrap an entire block of code in a try { ... } catch(e) { ... } to keep the noise down on individual function calls that may trigger an error…

is there some way to do this in zig; and if so, is it a “best practice”???

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

In the same vein as @AndrewCodeDev’s second pattern, I would say that a simple Zig way to emulate try/catch would be to put the group of function calls in a wrapper function and then you only catch once:

fn foo() !void {}

fn bar() !void {}

fn baz() !void {
    return error.Boom;
}

fn together() !void {
    try foo();
    try bar();
    try baz();
}

pub fn main() !void {
    together() catch |e| {
        std.debug.print("error: {}\n", .{e});
    };
}
2 Likes

i was hoping there was some labeled block idiom, which is sort of an unnamed function…

@biosbob, Also, you can do this as well (forgot to mention):

Here, we break the block and keep the error union and then unpack/switch over it.

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;
    };

    if (result) |out| {
        std.log.info("Out {}", .{ out });
    } else |err| {
        std.log.info("Error: {}", .{err});
    }   
}
2 Likes