.init shorthand works with `try` but not with `catch`

I ran into a small but surprising sharp edge related to the .method shorthand when combined with error handling.

I sometimes use the shorthand form:

const name: struct_with_method = .method();

When working with std.unicode.Utf8View, I needed to handle errors.

Using try works as expected:

Working code

const std = @import("std");

test {
    const s = "zig";
    const view: std.unicode.Utf8View = try .init(s);
    var iter = view.iterator();
    std.debug.print("{s}\n", .{iter.peek(1)});
}

However, when I tried to handle the error explicitly with catch, I expected the equivalent form to work:

Non-working code

const std = @import("std");

test {
    const s = "zig";
    const view: std.unicode.Utf8View = .init(s) catch |err| switch (err) {
        .InvalidUtf8 => {
            return error.InvalidRune;
        },
    };
    var iter = view.iterator();
    std.debug.print("{s}\n", .{iter.peek(1)});
}

This fails with:

prog.zig:5:41: error: type '@EnumLiteral()' not a function
    const view: std.unicode.Utf8View = .init(s) catch |err| switch (err) {

Question

Is this difference between try and catch with the .method shorthand intentional?

I understand how to rewrite this using the fully qualified call, but I was surprised that the shorthand works in one case and not the other, and wanted to check whether this is expected behavior or a known sharp edge.

1 Like

Yes, it’s intentional. Quoting the bulk of it below:

In general, syntax forms which “extract” components of an operand cannot provide result types to said operands. For instance, a field access x.foo clearly cannot provide a result type to the expression x, as there may be arbitrarily many aggregate types with a field named foo. In general, the same is true of error handling: for instance, x catch ... cannot provide a result type to x even if the overall expression has a result type, because while this type may contain sufficient information to determine the payload type, we cannot determine the error type.

However, there is one interesting exception here! The try operator could, in theory, provide a correct result type to its operand. If an expression try x has result type T, then the sub-expression x can be assigned result type E!T, where E is the error set of the current function. Because the behavior of try is more limited than that of e.g. catch, we may not know the error set type that the expression will actually return, but we do know that it must be coercible to the error set of this function, and we lose no information from applying this rule (the result would have been coerced to this type by the ret_node ZIR in the error path regardless). This coercion will correctly handle growing an IES, both runtime and ad-hoc. The more I think about it, the more this seems to me like a no-brainer.

5 Likes

Your second example fails because without try, .init(s) looks like you’re trying to use an enum value called init as if it were a function, which isn’t valid in Zig.

That doesn’t answer the question of why it doesn’t work at a useful level, but just repeats the compiler error—which I’m sure OP encountered already given that they said it’s not working.

One could also say that try .f(...) also looks a lot like an enum literal being called, but the question OP is actually asking is “why does .f(...) catch ... look like an enum literal to the compiler, but try .f(...) doesn’t?” And the answer to that involves result types as I quoted in comment above.

2 Likes
const a: ?Foo = Foo.init() catch null;

Given the existence of such expressions, it is difficult for us to use RLS to infer the type of the expression before catch.

Edit:

After careful consideration, I think this example doesn’t really hold up, because try can actually face similar issues, and try can handle this kind of result type quite well.

1 Like