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

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

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

If the block does not return anything other than the possible error, you can also just do

(blk: {
    mayfail1() catch |err| break :blk err;
    mayfail2() catch |err| break :blk err;
}) catch |err| std.debug.print("handle error: {}\n", .{err});

There has been a proposal (Improve block-local error handling · Issue #5610 · ziglang/zig · GitHub) to add a variant of try for leaving labeled blocks, so you could simply do try :blk mayfail1(); but this was unfortunately rejected, so I guess the catch |err| break :blk err remains idiomatic now.

BTW a footgun to be aware of is that returning an error from a block via break does NOT trigger errdefer:

(blk: {
    errdefer std.debug.print("errdefer\n", .{}); // NEVER CALLED!
    mayfail1() catch |err| break :blk err;
}) catch |err| std.debug.print("handle error: {}\n", .{err});

I haven’t actually tried this, but can’t you use errdefer like this as well?

{
    errdefer |err| switch(err) {
        // handle
    }

    try something();
    const stuff = try getStuff(); 
} 

This does work. It’s a pattern I use occasionally when I want to treat all subordinate calls as erroring the same way.

On master capturing errdefer is already removed.

Good call. I actually don’t capture the error when using this pattern. I only use it when I want to treat all errors as being the same, which is similar to how I used try/catch in other languages.

this sounds like a bug to me!

There is also

_ = blk: {
    failing_function0() catch |err| break :blk err;
    failing_function1() catch |err| break :blk err;
    failing_function2() catch |err| break :blk err;
} catch {
    // handle
};

It seems like at a glance, but I am not so sure. it is just catching the error and assigning its value to a variable, which is a perfectly valid thing to do, not the errdefer being triggered by the try, which is control flow.

It isn’t uncommon when wrapping C libraries to create functions that convert C error codes into Zig errors, and it isn’t an error to return from them.

pub const Error = error{
    OutOfMemory,
    InvalidArgument,
};

fn getError(errno: c_int) ?Error {
    return switch (errno) {
        0 => null,
        1 => error.OutOfMemory,
        2 => error.InvalidArgument,
        else => unreachable,
    };
}

pub fn main() !void {
    // No try/catch, though returns an error
    if (getError(0)) |_| {
        unreachable;
    }
    if (getError(1)) |err| {
        std.debug.print("Error name: {s}\n", .{@errorName(err)});
    }
    std.debug.print("Hello world!\n", .{});
}
Error name: OutOfMemory
Hello world!

EDIT:
I have given this some more thought, and now I am even less sure. Perhaps this is a bug. The lack of type resolution and which allows for the outer catch to be triggered is throwing my mind off.

The thing which sounds like a bug to me is the errdefer not running when the block is exited with an error.

I was thinking this too, but then the block it is being run within has a catch with no return type to infer. My thought was that perhaps the discard is just swallowing it up as a value and not interpreting it as an error condition to trigger the errdefer.

The more I think about it the more I lean towards agreeing it might be a bug. It doesn’t seem very well-defined if it isn’t.

I wonder if adding a scoped-try to Zig is a worthwhile idea. So your example hypothetically could be written something like:

_ = blk: {
    try :blk failing_function0();
    try :blk failing_function1();
    try :blk failing_function2();
} catch |err| {
    // handle
};

see above:

This is totally unexpected for me. I now heavily rely on this way of propagating block-level errors to eliminate duplicate error-handling schemes. If this is true, my code will no longer be reliable.
I really hope this is just a bug and not a semantic issue.

The documentation says that errdefer “evaluates the deferred expression on block exit path if and only if the function returned with an error from the block”; it does just that so it is technically not a bug, because a labeled block is not a function.

But of course if we think of labeled blocks as “manually inlined functions” or “immediately called nested functions”, we expect them to work like functions and break :label <value> to be equivalent to returning a value from a function, so it is unexpected that errdefer is not triggered here.

To add to this, my understanding is that unlike Go’s defer which is tied to function return, Zig’s defer is tied to block scope exit. It would then seem very weird to me that Zig’s errdefer could be tied to function scope.