A few times I forgot to add try before a function call whose result I would store in a variable. Compiler would then report an error but not on that line, it would report an error at the place where I used that variable because it was not of expected type there.
That got me thinking. If compiler would not allow assigning error unions to variables/fields (error union types would only be allowed on function returns) it could then report the error on the line where I forgot to add try or catch.
So can anyone point me to some Zig code in std lib or anywhere else where being able to assign error unions to variables is actually useful? Or does someone know enough of std lib to be able to claim that functionality really is not ever used?
That would mean you can’t write a function that handles the error handling and pass it the error union, so basically the moment you have some code where you want to handle a set of errors in the same way in multiple places, you instead would have to duplicate code.
It would also mean that you can’t call the function without using try, catch or if(<error_union>) immediately.
I think that could potentially make it so that generic calls like this would now have an awkward corner case of being illegal, if the return type is an error union:
const result = @call(.auto, function, args);
switch(@typeInfo(@TypeOf(result)) {
...
}
So I think it would complicate things for generic programming unnecessarily.
Also sometimes you just want to use the error union as a value, for example I would imagine if people write their own testing utility functions that those functions may expect an error union as argument and then just do things like print some error report that gets displayed to the user in some custom testing setup.
I forgot to think about function parameters. You make good points though after thinking a bit through it I might see some solutions.
First regarding writing a function that handles the error. If its purpose is handling the error it only needs the error value, not the whole error union and so can be used in multiple places like this:
mightReturnError() catch |err| handlerError(err);
Now your generic example was a bit harder. At first I though that since you are assigning the result of the @call to the result variable you are also assuming that function returns something which might also not be true but then I tried it and discovered you can actually assign a void to the variable. Maybe then the compiler could also allow try/catch with functions that don’t return errors and would just ignore them in that case. That way if you use @call in generic code you are expecting functions that can not return error but if for example you are using try @call that means you are allowing both functions that can and that cannot return error.
I agree that this would not be ideal since if that is allowed for regular calls as well compiler would not report errors when you call function that does not return error with try or catch which is useful.
I also searched @call through Zig’s source and I found only one place in Thread.zig which handles the situation that you are mentioning but by first checking
and then using @call with catch in .error_union branch. So it can always be done like that.
So I understand your arguments and even though they are valid if people don’t actually ever use them like that then in practice we wouldn’t lose anything with this approach.
The compiler can become smarter.
When E!T is used where T is expected, compiler can trace back and see if there was an assignment to an untyped constant or variable where a try can be inserted to fix the problem.
Some improvement in the error message for using an error union when the type is expected would be helpful, sure.
I don’t think what we have is so bad, though, one example:
fn missedError() !usize {
if (false) return error.MissedAnError;
return 5;
}
test "missed error message" {
var is_five = missedError();
// error: invalid operands to binary expression: 'error_union' and 'comptime_int'
is_five += 7;
}
But when you don’t use try or catch, the error union is the type of the variable. I think it helps to understand this: try and catch are control flow which special-cases error unions, they don’t change the result type, they do things when the result type resolves to an error.
We don’t want a type system where a type which exists, has a size, can be assigned, passed around, etc. can’t be expressed as a variable. That isn’t a good solution to inscrutable compiler errors (although after you forget try a few times it’s pretty clear what the compiler is saying).
One suggestion which hasn’t been made yet is to use ZLS and turn on type hints. That will clearly show you when the type of a variable is an error union, so you don’t have to wait for the compile error to fix it.
Just a thought: There could be a requirement to add something (maybe an !) to the variable declaration to indicate that this variable is an error union. That way you would always get the error on that exact line if you just forgot a try or catch and the variable does not have that marker. It’s really just a thought though! I have no idea in how many ways this would be a bad idea
What I meant was a requirement to specify if a variable is allowed to hold an error union. In your example, const result = failingFoo(); would compile just fine. In my example, it would be like this:
const result = failingFoo(); // does not compile
const result = try failingFoo(); // result is a bool
const result = failingFoo() catch unreachable; // result is a bool
const! result = failingFoo(); // result is error union
My main point is the requirement part. Both of your examples require the user to actively opt-in by specifying the type of the variable. Take this example from earlier:
test "missed error message" {
var is_five = missedError();
// error: invalid operands to binary expression: 'error_union' and 'comptime_int'
is_five += 7;
}
You get the compile error on the line with the addition, not the one with the variable declaration. The OP said that this was their main problem. To solve that, you could start specifying every type of every variable explicitly which is your suggestion, I think. My idea is that we require adding a ! to the var/const if the type is not specified, but an error union is expected.
fn thing() CorrectType {...}
fn errorThing() BadError!CorrectType {..}
// in current Zig, these lines are equivalent
var x = thing();
var x: CorrectType = thing();
// and these are as well
var x = errorThing();
var x: BadError!CorrectType = errorThing();
// in *my* version, this line does not even compile
var x = errorThing();
// and these line are equivalent
var! x = errorThing();
var x: BadError!CorrectType = errorThing();
Oh I see, I personally never ran into a situation where that felt needed, I honestly prefer the try, and I think we ought to be satisfied with what we have here, because you suggestion seems to be syntactic sugar for syntactic sugar, try strikes a good balance, I don’t use it all the time nowadays as I try to handle things more locally but in any case adding another way of declaring that something can have an error seems unecessary at least, I fail to see a situation where this convey the fact that It is expected to have an error union where try wouldn’t work well. Is there a particular use case that you have in mind which this syntax solves ?
I think @pierrelgol makes a good point, the type system already indicates if a variable is an error union or not. I don’t like the idea of adding more syntax to do the same thing. Further it seems like a heavy handed solution to “the compile error message is confusing”.
FWIW, I think I’m error is quite straightforward. And it’s pointing at the right spot. There is no way for the compiler to know you intended to use try or catch and forgot. It’s saying it expected one type and got another, which is the error. I think it could be better if the message was concise, so that it didn’t look overwhelming.
First, I want to stress that I’m not saying we should change anything. I just had an idea to address the issue of the OP. But yes, I also sometimes have problems with how it currently works. Here is an example. Codebase starts like this:
It doesn’t compile. That’s good! But what is the error?
error: cannot format error union without a specifier (i.e. {!} or {any})
Sure, this example is very tiny and even in real world examples, I’m always able to track down the problem, but it would be better to have the error on the line where I declared the variable. With my “proposal” from above, you would get a compile error on the declaration line because there is no const!, but just const.
Ok I see your point better, but I have to say that I think that as mentioned above by @Calder-Ty The error message is actually correct here, the compiler wanted one type and you provided something else, it’s hard for it to make reasonable assumptions about your intent, at least I don’t see a heuristic that would provide a good answers, but I agree that it could do more, by providing a suggestions akin to what Rust error messages often do. Things like “here’s your error, and on what line it is” and then “hey the variable was declared up here are you sure you didn’t forget to put a try ?”
Because in my book, syntax should be the last resort for solving a problem, however this seems like a good fit for an improvement of the tooling. Which if I remember from reading issues on github is actually something that is suppose to happen at some point near 1.0 or maybe just after, when they will invest more time in solidifying the std, and also improving error message and stuff. So who knows maybe in a few years this problem will be solved by a better error message.
I agree with your points @Calder-Ty and @pierrelgol. I do think that it could be nicer, but I also don’t think that the language itself should be made more complicated for this.
Things like “here’s your error, and on what line it is” and then “hey the variable was declared up here are you sure you didn’t forget to put a try ?”
This sounds like a very good idea to me. The language is unchanged and you still have the actual error while also being pointed to the line where you might have made the mistake. Until we have something like that, I’ll happily live with the current state