by omani on 1/28/25, 3:26 PM with 91 comments
by floating-io on 1/28/25, 4:18 PM
foo ? { bar }
Reads naturally to me as "If foo then bar", when it's actually "If foo's error return exists then bar". I would suggest a different operator character, because this one reads wrongly IMO.Maybe it's just because I originally come from C, where `foo ? bar : baz` meant "if foo then bar else baz", but the fact remains...
by teeray on 1/28/25, 3:55 PM
Once you've been using the language for awhile, you begin to dislike the elaborate system of rugs other languages have to sweep errors under. Errors in Go are right there, in your face, and undeniable that the operation you are doing can be faulty somehow. With good error wrapping, you can trace down exactly which of these `if err != nil` blocks generated the error without a stack trace. If it bothers you that much, you can always make a snippet / macro for it in your editor.
by theamk on 1/28/25, 3:59 PM
We've converted a few scripts and webapps from Python to Go, and if one does default handling ("return err") the error logs became significantly less useful, compared to exception backtraces. Yes, there are ways around it, but most tutorials don't show them.
by sigmonsays on 1/28/25, 4:37 PM
Go error handling should remain simple, like the language.
These are all tools, just pick the one you like and stop trying to make them like others.
by RedNifre on 1/28/25, 3:50 PM
I find this a bit odd. Isn't the idea of the primitive error handling that it is obvious and easy, as in "functions can return multiple results, a popular pattern is to return the good result and the error as two separate nullable values of which exactly one will be not null, so you can check if err == nil."?
If you go with fancy error handling anyway, how is this '?' better than returning a Result and do something like foo().getOr { return fmt.Errorf("Tja: %v", err) }
by cratermoon on 1/28/25, 3:45 PM
by 827a on 1/28/25, 5:16 PM
Criticism:
> Within the block a new variable err is implicitly declared, possibly shadowing other variables named err
Shadowing here is strange, and I would prefer a design where it did not shadow other variables named err, but rather threw a compiler error concerning the re-declaration of a variable. That would effectively mean that you can't mix-and-match this syntax with old error-handling inside one function, because code like this would fail to compile:
func Test() {
user, err := GetUser("12345")
if err != nil {
panic(err)
}
EmailUser(user) ? {
panic(err)
}
}
I'm fearful the shadowing will be confusing, because one might try to reference that shadowed error within the block in (rare) situations where you need to return the synthesis of two error values, and you'll need to know the trivia of: `err` is a special name, I shouldn't name that shadowed variable `err`, let me name it `err2`. Granted: throwing a compiler error would also disallow this and force you to name the first variable `err2`; but at least the compiler is telling you the problem, rather than relying on your knowledge of new trivia.by zimbatm on 1/28/25, 4:51 PM
I'm not going to invoke the slippery slope argument, but what distinguishes Go from the pack is how explicit it is. It can make it more tedious to write, but also much easier to follow as a reader.
by vrnvu on 1/28/25, 4:12 PM
"The goal of this proposal is to introduce a new syntax that reduces the amount of code required to check errors in the normal case, without obscuring flow of control."
The key is "check errors in the normal case".
When the core principles of Go have always been simplicity, flexibility, and having one way of doing things, this feels completely like a step in the opposite direction. We will have syntax sugar for "normal cases" while still relying on the `if err != nil` block for everything else. It’s similar to how we now have both `iterators` and `for loops` as constructions for loops.
by jchw on 1/28/25, 4:02 PM
> Disadvantage 4: No other block in Go is optional. The semicolon insertion rule, and the fact that a block is permitted where a statement is permitted, means that inserting or removing a newline can convert one valid Go program into another. As far as I know, that is not true today.
Yeah, this seems like a big problem to me, personally. Go has a fair number of lingering foot guns but this is one too far IMO. I think the no-block case should require something else to follow it, perhaps the return keyword. That'd also help prevent it from being as easily missed...
by thiht on 1/29/25, 12:13 AM
But my biggest beef is the implicit variable declaration, I can’t stand it. That’s just lazy, bad design.
That’s not a great proposal overall, and I suspect if the same proposal had been made by someone else outside of the Go core team, we would have not heard of it.
I hope it gets rejected.
by high_na_euv on 1/28/25, 4:37 PM
In my opinion everything should return type like MayFail<T>, Result<T>
by jasonthorsness on 1/28/25, 4:31 PM
by rigelbm on 1/28/25, 4:18 PM
by TriangleEdge on 1/28/25, 4:15 PM
The proposal is nice, but a bit shallow.
by evanmoran on 1/28/25, 5:04 PM
To demonstrate my tweak to your idea, imagine this example code:
r, err := SomeFunction() if err != nil { return fmt.Errorf("something 1 failed: %v", err) }
r2, err := SomeFunction2() if err != nil { return fmt.Errorf("something 2 failed: %v", err) }
r3, err := SomeFunction3() if err != nil { return fmt.Errorf("something 3 failed: %v", err) }
In the current proposal it would turn into this:
r := SomeFunction() ? { return fmt.Errorf("something 1 failed: %v", err) }
r2 := SomeFunction2() ? { return fmt.Errorf("something 2 failed: %v", err) }
r3 := SomeFunction3() ? { return fmt.Errorf("something 3 failed: %v", err) }
My first suggestion is to keep `err` variables visible. It ends up being not much longer, but it is much more readable and Go-like:
r, err := SomeFunction() ? { return fmt.Errorf("something 1 failed: %v", err) }
r2, err := SomeFunction2() ? { return fmt.Errorf("something 2 failed: %v", err) }
r3, err := SomeFunction3() ? { return fmt.Errorf("something 3 failed: %v", err) }
My second suggestion is to require ? to always have a block, and also allow them to "chain" so only the last statement needs a block:
r, err := SomeFunction() ? r2, err := SomeFunction2() ? r3, err := SomeFunction3() ? { return fmt.Errorf("something 1, 2 or 3 failed: %v", err) }
As you can see this is much shorter! Having the block is always required at the end of the "chain" of question mark statements is more consistent with how `if` statements require a block currently. It also makes the `return err` flow also always visible (no return magic). It also also has a huge advantage of it being much harder to miss a question mark syntactically. as a question mark without a block would be a syntax error.
For example, this is an error:
r, err := SomeFunction() ? // <-- compile error: missing block after ?
And also this is an error:
r, err := SomeFunction() ? r2, err := SomeFunction2() // <-- compile error: missing block after ? r3, err := SomeFunction3() ? { return fmt.Errorf("something 1, 2 or 3 failed: %v", err) }
Thanks for listening! Curious what folks think.
by qaq on 1/28/25, 4:20 PM
by reactordev on 1/28/25, 4:49 PM
by pphysch on 1/28/25, 4:00 PM
But now we have LLM copilots, so writing boilerplate in any language is dramatically more optional.
And I don't see this proposal significantly improving readability of the code.