Wondering; why try and not .!?

Maybe this has been discussed already somewhere else but it’s difficult to search for given the symbols involved, so here goes: I suddenly realized a pattern

optional.?
ptr.*

But then there’s

try errorunion

and not

errorunion.!

Wouldn’t the .! be more consistent with the other two and avoid the use of parenthesis when chaining on a method after the value has been successfully unwrapped from the error union?

(try errorunion).foo(); // before
errorunion.!.foo(); // after
4 Likes

Yeah, I think the primary reason for it has been that error handling should be more verbose so as to be easier to spot in the code. But I am starting to think that .! instead of try would be kinda nice, too.

1 Like

That’s a good reason indeed. So this would be once again a matter of weighing the tradeoffs: error handling visibility vs syntax consistency and ergonomics. It’s a tough call to make.

1 Like

I think it’s designed explicitly to prevent chaining in order to break expressions where errors can occur.

Additionally, the best place where to handle an error is not necessarily where you want to use the non-error value. The dot bang syntax will tempt you into delaying the try logic until you need the non-error value.

4 Likes

Agreed, but this made me think some more, and I come to the realization thet try is actually not for handling an error “on the spot”, but rather to bubble it up so a higher level in the call stack handles it. In other words, a try is like saying “give me the value or bail out immediately”, which in my mind fits well into an expression chaining scenario.

catch on the other hand forces you to handle the error on the spot even though it’s often used as a provide default value on error mechanism when used without the error capture (or even also a bail out immediately mechanism too when used with return or unreachable). So in this sense your reasoning would better apply to catch than to try, right?

1 Like

Maybe, but I think it still applies when you think about sections of a function that can fail vs sections that can’t.

Imagine a function as a sequence of statements that can either fail (x) or not (-).

Everything else being equal, I think having one critical section of a function past which nothing can fail is preferable to having trys sprinkled everywhere.

// fn 1
----xxxxxxx-------------------

// fn 2
--x---x----x---xx---x------x--

The fn1 seems to me to have simpler error-related control flow.

This is kinda what all the AssumeCapacity functions in ArrayList give you.

3 Likes

I see. And I think I found another reason why .! would be a problem: it doesn’t play well with errdefer (and specifically when needing to free on error).

Status quo:

const ptr = try allocator.create(Node);
errdefer allocator.destroy(ptr); // no leak on error
_ = try ptr.returnsPtrButCanFail();

with .!:

const ptr = allocator.create(Node).!.returnsPtrButCanFail().!; // leak on error

I guess that like in other languages (C++ I’m looking at you) you could have both options, recommending .! only be used when you don’t need errdefer but that doesn’t seem like tha Zig way of doing things. It’s better to have only one way of doing things; it’s a win for code clarity, simplified parsing, analyzing, etc.

2 Likes

In Zig, keywords are used for operators that affect control flow and symbols are used for operators that do not affect control flow.

This is also why we have boolean operators (and, or, !) rather than (and, or, not).

12 Likes

Very interesting! Another pattern and consistency I wasn’t aware of. Good to know.

1 Like

Not sure if this is relevant, but I think having the try is what allows you to optionally catch the errors when running error-prone statements. So…

while (true) {
   reader.streamUntilDelimiter(input.writer(), '\n', 1000) catch |err| switch (err) {
      error.EndOfStream => {
         std.debug.print("\n", .{ });
         break;
      },
      else => |e| return e
   };

   ...
}

or

while (reader.streamUntilDelimiter(input.writer(), '\n', 1000) != error.EndOfStream) { ... }

I’m not sure how this would be accomplished if the errorunion used the dot syntax

1 Like

I’m often visually scanning for try to see control flow that would be much harder to see with .!.

It’s an interesting idea though and does feel nicely orthogonal to the rest.

4 Likes

I like how Swift has try, try? and try!.

The first one is like in Zig, the second one returns null in case of error and the last one panics.

That said I don’t think those would be a particularly good addition to Zig though.

2 Likes

Oh, that’s interesting, I didn’t know that. But I agree, it’s certainly not for Zig. Coding in Zig for me has always felt the best exactly because it’s offered only the leanest tools, that way forcing me to write minimal and readable code even though it sometimes took me a moment to see why that was. But it’s always so cool to realise that Zig already has the foundation of very thorough design decisions!

2 Likes