More syntactically cleaner way to check if a something is an error from an error union

I am new to Zig so I might be wrong here but I want to know is there a cleaner way to check if the return value of a function returning a error union is actually an error.? To describe more effectively, here is a an example:

fn return_error_union() !void {
    return error.InvalidInput
}

Now to check if the above function returns an error and use that to later take some sort of decision down below, you have do this kind of boilerplate code:

    var result: bool = undefined;
    if (return_error_union()) |_| {
        result = true;
    } else |_| {
        result = false;
    }

And this is what you get in a if statement

const a: u8 = 5
const b: u8 = 10;

if (a == 5 and b == 10 or result) { ... }

Now imagine having just two or three such functions in single a single conditional.

This feels more like Rust’s if let Ok(_) = some_func() { ... } else { ... } when what I really want is something like some_func().is_ok() / some_func.is_err().

Please give your comments, suggestions below.

Thanks

Hello @AMythicDev Welcome to ziggit :slight_smile:

I am adding another example function:

fn error_or_int() !i32 {
    return 42;
}

There are various ways to handle an error, besides if.

Using catch

return_error_union() catch |err| { do_something(err); };

const num = error_or_int() catch |err| { do_something(err); };

Using try

If the error union is an error, the error value is returned to the caller.

try return_error_union();

const num = try error_or_int();

it is the same as:

return_error_union() catch |err| { return err; };

const num = error_or_int() catch |err| { return err; };

The rule is:

  • If you cannot handle the error use try to return it
  • If you can handle the error use catch
  • If you don’t expect an error to happen use catch unreachable

EDIT:

I don’t recommend its use, catch and try is better.

fn is_ok(v: anytype) bool {
    _ = v catch return false;
    return true;
}

fn is_err(v: anytype) bool {
     _ = v catch return true;
     return false;
}
1 Like

The best we can do is this:

const result = if (return_error_union()) true else |_| false;
1 Like

I am not sure how you’re able to get this but AFAIK catch requires that it’s right arm must return a value of the same type as the ok value of the error union. So it might work for return_error_union() as it’s ok return value is void but it won’t work for the error_or_int() function.

EDIT: Nor I like the notion of putting entire program logic blocks inside a catch block. Also it feels more of a else clause of an if statment with stricter type requirements. And it’s always a good practice to avoid elses since they introduce deep nesting within them.

The rest of your answer gives a good insight on various error handling strategies but it doesn’t really account much with my actual question up top.

Seems concise then my code but can’t say it’s the most elegant.

You might need to go into more detail about what exactly your use case is. What are you planning on doing with the result of is_ok() or is_err()?

const val = try some_func();
// we are now certain that some_func did not error
const val = some_func() catch |err| {
    // within this block we can be certain that some_func returned an error
};

If your intention is to check if a function returned a particular error during a test, then there’s std.testing.expectError:

try std.testing.expectError(error.Foo, some_func());
2 Likes

Did you miss the implementation of is_ok and is_err at the end of my post?

Example usage is:

const std = @import("std");

fn return_error_union() !void {
    return error.InvalidInput;
}

fn is_ok(v: anytype) bool {
    _ = v catch return false;
    return true;
}

pub fn main() void {
    if (is_ok(return_error_union())) {
        std.debug.print("OK\n", .{});
    } else {
        std.debug.print("NOT OK\n", .{});
    }
}
1 Like

Yeah sure. Here’s a stripped down version of a place where I have been using this pattern

pub fn check_install_name(name: []const u8) bool {
    ...
    var components = std.mem.split(u8, name[4..], "-");
    ...
    const version = components.next() orelse return false;
    var sv = true;
    if (std.SemanticVersion.parse(v)) |_| {
        sv = true;
    } else |_| {
        sv = false;
    }
    if (!streql(v, "stable") and !streql(v, "master") and !sv) {
        return false;
    }
    return true;
}

streql is basically a wrapper around std.mem.eql(u8, []const u8, []const u8)

This is just one example, I have used a similar pattern couple of more times.

1 Like

Yeah I did take a look at it and I am unsure what to say about it. It’s so simple straightforward that it should be the part of Zig stdlib rather than me having it in my simple project.

Well, I don’t know what the rest of the function is doing, but the excerpt that you showed would be more elegant like so:

pub fn check_install_name(name: []const u8) bool {
    var components = std.mem.split(u8, name[4..], "-");
    const version = components.next() catch return false;
    _ = std.SemanticVersion.parse(v)) catch 
        return (streql(v, "stable") or streql(v, "master"));

    return true;
}

It’s also more efficient as it short-circuits the streql if the parse succeds.

3 Likes

I think the first step is to simplify the boolean logic, rewritten this becomes:

fn is_ok(v: anytype) bool {
    _ = v catch return false;
    return true;
}
pub fn check_install_name(name: []const u8) bool {
    ...
    var components = std.mem.split(u8, name[4..], "-");
    ...
    const version = components.next() catch return false;
    return streql(v, "stable") or streql(v, "master") or is_ok(std.SemanticVersion.parse(v));
}

Don’t use if to branch just to return true or false again.

I don’t think a function like is_ok needs to be part of the standard library, usually I would just bubble the error up when there is an error and then actually do something with the error, instead of converting it to a true or false. It is better to handle the error somewhere then to convert it to a boolean, possibly forgetting to handle it.

I think it is so simple that there is no point in making it part of a library and not the usual way to handle errors in Zig.
Is ok seems to be a Rust pattern, I think using try and handling errors directly is more Zig idiomatic.

I might even change the code towards something like this:

pub fn check_install_name(name: []const u8) !void {
    var components = std.mem.split(u8, name[4..], "-");
    const version = try components.next();
    const v = ...;
    if(streql(v, "stable") or streql(v, "master")) return;
    _ = try std.SemanticVersion.parse(v);
}

So check is just void on success and if there is an error it gets handled somewhere, whether you recover from it, or catch it and print and abort with it, is decision of the caller.

Why use booleans to effect control flow if you already have error values that can do that directly and with support for multiple different ways to handle those errors?

There can be situations where it makes sense, to drop errors or convert them to different errors, for example in this case it might make sense to convert some detailed error into some more general error.UnknownVersion or something like that.


Wait a minute, I haven’t really used std.mem.split but it returns a normal iterator, so next returns an optional not an error!

const version = components.next() orelse return error.UnknownVersion;
4 Likes

SplitIterator.next returns an optional so this has to be:

components.next() orelse ...

Replacing Zig’s builtin error handling mechanisms with other techniques from other languages will not yield idiomatic Zig code. I think that sticking to Zig’s way is indeed elegant:

pub fn check_install_name(name: []const u8) !void {
    var components = std.mem.splitScalar(u8, name[4..], '-');
    const v = components.next() orelse return error.NoVersion;
    if (streql(v, "stable") or streql(v, "master")) return;
    _ = try std.SemanticVersion.parse(v));
}

Here, the errors will tell you what actually happened.

5 Likes

Ahh sorry I edited that line in the discourse editor and typed out the wrong thing.

EDIT: Fixed in my original post.

Ok so I see people mistook this line by thinking that it is line with any extra logic.

std.SemanticVersion.parse(v)

So I want to clarify some stuff about why its there: I simply want to check if the version is a valid semantic version or the text master or stable. If its matching none of the criteria, we want to return false. Hence I really don’t care what errors this line gives, if its an error we simply return false.

I liked your solution, seems quite clean. But again not a simple is_err() like thing

Like it or not, the language doesn’t let you elegantly discard potentially meaningful information. If a function returns an error union, the assumption is that the actual error is pertinent.

2 Likes

Yup I like how it doesn’t allow unused return types at all but it does have _ = to sorta get around that and that’s similar to what I need. A simple function that tells if something is an error or not. I don’t really care about the error, just want to know if it’s an error. It’s not even in my core program error handling part. It only comes because std.SemanticVersion.parse() returns a error union.

1 Like

Hi @AMythicDev,

I think, as others have indicated, there is no option for a method like semantics on error unions in zig. As others have suggested, there are alternative ways to do what you want that you might find more or less appealing.

However I wanted to add some thoughts that might help to think about this. You make the allusion to Rust where one can take an error and do is_ok() or is_err() checks. However zig Errors and rust Results are not really interchangeable. Rust’s result types are just a specific Rust Enum. There isn’t really anything ‘special’ about them. Zig’s Error Unions are special. They are Unions, but they aren’t (AFAIK) the same as any Zig Union. This is what allows zig to do special syntactical and semantic things with them (such as error capturing in the else clause of if statements, something you can’t do with regular unions).

Secondly, Rust and Zig just view these basic structures differently. In Zig there is a much stronger attitude of ‘What you see is what you get’. There isn’t extra methods or code associated with structs/enums/unions, unless you put it there. By default structs/enums/unions are just data. They can be interacted with differently, but for the most part, it’s just data.

Hope this is helpful, and safe zig travels!

3 Likes

You could achieve that with a function pretty easily:

fn isSemVer(v: []const u8) bool {
    _ = std.SemanticVersion.parse(v) catch return false;
    return true;
}

test isSemVer {
    try std.testing.expect(isSemVer("1.1.0"));
    try std.testing.expect(!isSemVer("1.oops.0"));
}

I like to factor control flow which isn’t part of a function into its own function, even if it’s small like this. Tiny functions are harmless, the compiler will usually inline them.

This sort of points at the reason why there isn’t a standard library way to turn errors into booleans. There’s just no reason to have that, it’s trivial to write a general-purpose one, but it will generally make more sense to encapsulate the error-returning function as shown above, to turn it into a bool.

4 Likes