Is it bad to put try in front of “everything”

Hello, I am new to zig and whenever writing code of it errors I just put try in front of it, is it a bad practice to do so?

If so what are the alternatives?

3 Likes

Hi, try is definitely fine, it’s actually somewhat of a default error handling method in Zig because it simply propagates an error up the stack.

There’re two alternatives, though:

  1. catch - this one allows to provide a default value or execute a block expression upon error.

  2. if + switch - this one allows to switch on specific error values and handle them differently.

Either one of these alternatives could be used to ignore an error, which I’d say is a bad practice. So try not to use too much of:

mayError() catch unreachable;
if (mayError()) |ok| {
    _ = ok;
} else |_| {}

However, the single valid case of discarding errors like this, which I’ve encountered, is in a function that can’t return errors by design.

UPD:

Here’s another way to ignore an error:

mayError() catch {};

Still not advised, but it’s arguably better than the first alternative since hitting unreachable in ReleaseFast mode is UB.

7 Likes

I’m going to take the opposing view and advocate that we should not put try in front of everything.

As a developer, I have to read large codebases every day for work. It’s important that I know (at a glance) what can and cannot generate errors. This is my major complaint with throws - it obfuscates where errors are coming from.

So in one sense, there is an obfuscation where no signals are provided. In another, there is an obfuscation where too many symbols are used. At some point, they lose their communicative meaning even though they compile.

That said, I have used this mechanism once in the opposite way - for certain callbacks, they have an error signature to match the function prototype, but some callbacks don’t technically return an error because they do trivial work. I’m quite happy with this, however, because generically speaking, the function being pointed to could return an error (and once you’re down to pointers, it’s hard to tell which one is which). In this case, it’s good to treat every gun like it’s loaded.

I think try should be limited to where we can fail. It communicates something very important that is worthy of notice :slight_smile:

2 Likes

This is already enforced by the compiler.

fn foo() void {}

pub fn main() !void {
    try foo();
}
test.zig:6:12: error: expected error union type, found 'void'
    try foo();
        ~~~^~
4 Likes

Yup, you are correct. I think I misunderstood what the first post was trying to ask (I took it in a more philosophical direction). To the original topic though, if we trickle tries all the way up the code, then the only exit point is main (kind of like an uncaught exception). So on principal, we still shouldn’t just rely on try alone.

There’s going to be situations where this is just inappropriate. For instance, if there is a failure after a related allocation, memory can be leaked by just returning from the function. In that case, we need to catch and handle further issues. One way of doing this is errdefer but that introduces a mechanism outside of try that has to be considered. So even there, using try in front of “everything” (as the topic implies) is still not a good stand-alone idea.

Thanks for reply, @squeek502 - helped me understand the question a bit more clearly :slight_smile:

1 Like

Right, I think the best way to put it would be that try should be your default error handling method, and anything other than try should have some justification.

I remember something like this being said in one of Andrew’s Zig talks (something like “the laziest Zig programmer will still handle errors correctly because the easiest way to handle them is with try”) but I can’t find it right now.

EDIT: It was this talk (the part about errors):

6 Likes

Watched this talk the other day since you mentioned it, all I have to say is thank you very much. Create talk to watch.

3 Likes