Readit News logoReadit News
floating-io · 7 months ago
The biggest issue I have with this proposal is that reading the code in naïve fashion comes up with the wrong answer for me; YMMV. The proposed form--

  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...

kortex · 7 months ago
That is exactly how go usually works with error handling though.

    func mayfail() error {
        if snafu {
            return errors.New("oops")
        } else { return nil}
    }


    err := mayfail()
    if err != nil { handle }

Same as `mayfail() ? handle : happypath` would behave with lazy evaluation.

zemo · 7 months ago
Go doesn't have a ? operator today, and the ? operator being used for error handling has precedence in Rust and Zig, so it doesn't seem to be all that out of the ordinary or without precedent in other languages.
plorkyeran · 7 months ago
The proposed behavior is the opposite of how Swift and Rust use ?. `foo?.bar()` invokes `foo.bar()` in the non-error case, while in Go `foo ? { bar() }` invokes it in the error case.
toxik · 7 months ago

  foo else { bar }

jayd16 · 7 months ago
How about this? :)

    { foo } catch { bar }

nonethewiser · 7 months ago
This is probably the one that would anger Golangers the most lol
chairhairair · 7 months ago
It could be:

foo ?! { bar }

But, now we’re potentially confusing the “negation” and the “exclamation” meanings the bang symbol usually tries to communicate.

nonethewiser · 7 months ago
I tend to agree that ? looks like "if then" when what we really want is some sort of coalescing, or "if not then".

foo ?? { bar }

foo ?/ { bar }

foo ?: { bar }

foo ?> { bar }

foo ||> { bar }

Im not sure I like the idea at all though. It seems like a hack around a pretty explicit design choice. Although I do tend to agree the error handling boilerplate is kind of annoying.

xigoi · 7 months ago
V, which is syntactically similar to Go, uses

  foo or { bar }

teeray · 7 months ago
I feel like error handling in Go is divided between people who have been using the language for a long time, and those who are new to it. If you're used to exceptions, and languages with some kind of '?' operator, typing `if err != nil` all the time is probably excruciating. They seem to be the most vocal in the survey about wanting beloved error handling features from their favorite languages.

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.

madeofpalk · 7 months ago
I appreciate verbose and explicit patterns like this, but what go lacks is the algebraic data types/enum/unions to actually make this more ergonomic and checked.

I find it bizzare that go so strongly relies on this pattern, but lacks the features to make sure you actually check for errors.

nonethewiser · 7 months ago
Im not really following this. Only somewhat familiar with Go.

The pattern we're talking about is returning errors and having to explicitly check for them, right? How does the lack of "algebraic data types/enum/unions" make this pattern un-ergonomic?

javier2 · 7 months ago
100% this. The idea is ok, the tooling to actually use it is terrible
richbell · 7 months ago
> I feel like error handling in Go is divided between people who have been using the language for a long time, and those who are new to it. If you're used to exceptions, and languages with some kind of '?' operator, typing `if err != nil` all the time is probably excruciating. They seem to be the most vocal in the survey about wanting beloved error handling features from their favorite languages.

This implies that the only people who dislike Go's error handling are newbies that "don't get it".

Go's error handling is objectively bad for two reasons:

1. You are never forced to check or handle errors. It's easy to accidentally miss an `if err != nil` check, I've seen sages and newbies alike make this mistake.

> Errors in Go are right there, in your face, and undeniable that the operation you are doing can be faulty somehow.

2. Repeating `if err != nil` ad nauseam is not handling errors. Knowing the operation can be faulty somehow is a good way of putting it, because in most cases it's difficult — if not impossible — to figure out what specific failures may occur. This is exacerbated by the historical reliance on strings. e.g., Is it a simple issue that can be easily recovered? Is it a fatal error?

aiono · 7 months ago
> You are never forced to check or handle errors. It's easy to accidentally miss an `if err != nil` check, I've seen sages and newbies alike make this mistake.

While I also don't like Go's error handling approach I thought Go compiler gives an error if a variable is unused, in this case `err`. Is this not the case?

teeray · 7 months ago
> You are never forced to check errors…

There are linters that do, and I am of the opinion they should be added to `go vet`.

> Knowing the operation can be faulty somehow is a good way of putting it, because in most cases it's difficult — if not impossible

Guru was once able to tell you exactly what errors could be generated from any given err. Now that the world is LSP, we have lost this superpower.

Swizec · 7 months ago
Where I've found `?` super helpful in JS/TS and now miss it the most in Python is dealing with nested data structures.

``` if (foo.bar?.baz?.[5]?.bazinga?.value) ```

Is so much nicer than

``` if foo.bar and foo.bar.baz and foo.bar.baz[5] and foo.bar.baz[5].bazinga and foo.bar.baz[5].bazinga.value ```

I honestly don't care which one of those is falsy, my logic is the same either way.

This enables you to ergonomically pass around meaningful domain-oriented objects, which is nice.

Edit: looks like optional chaining is a separate proposal – https://github.com/golang/go/issues/42847

nonethewiser · 7 months ago
Yes, same experience. As someone who learned on python then got deep into Typsecript, this was a major bummer when returning to python.

Its funny because when I was a beginner in both I strongly preferred python syntax. I thought it was much simpler.

new_user_final · 7 months ago
You can use following syntax in python, ugly but better than multiple and (I don't use python anymore)

    if dict.get('key', {}).get('key-nested',{}).get....:

jakelazaroff · 7 months ago
I am but one lowly data point, but I've been using Go for a long time and the pervasive `if err != nil` is one of my least favorite parts of the language.
tdb7893 · 7 months ago
Yeah I've been using Go for years at multiple companies and I agree. Using multiple lines of code for something that's so common just isn't an efficient use of screen space to me and at a certain point all those lines hurt readability of code.
kasey_junk · 7 months ago
Big same.
pjmlp · 7 months ago
There are also those of us, old enough to have used Assembly as daily programming language, have used Go boilerplate style across many languages during 20+ years, and don't miss the days exceptions were still academic talk, unavailable in mainstream languages.
alekratz · 7 months ago
I'm not a Go programmer, but I feel like I've sort of "grown up" around them as the language has evolved. for a while I thought that the `if err != nil { ... }` was silly to put everywhere. As I've grown and written a lot more code, however, I actually don't see a problem with it. I'd even go as far as to say that it's a good thing because you're acknowledging the detail that an error could have occurred here, and you're explicitly choosing to pass the handling of it up the chain. with exceptions, there can be a lot of hidden behavior that you're just sweeping under the rug, or errors happen that you didn't even think could be raised by a function.
memset · 7 months ago
Super interested in your approach to error wrapping! It’s a feature I haven’t used much.

I tend to use logs with line numbers to point to where errors occur (but that only gets me so far if I’m returning the error from a child function in the call stack.)

teeray · 7 months ago
Simply wrap with what you were trying to do when the error occurred (and only that, no speculating what the error could be or indicate). If you do this down the call stack, you end up with a progressive chain of detail with strings you can grep for. For example, something like "processing users index: listing users: consulting redis cache: no route to host" is great. Just use `fmt.Errorf("some wrapping: %w", err)` the whole way up. It has all the detail you want with none of the detail you don't need.
mherkender · 7 months ago
I mostly agree but I wish it could be more automatic. I like Golang's error system I just wish they'd provide shorthand ways of handling them.
theamk · 7 months ago
It saddens me tbat the default error handler is "return err", as opposed to something that appends context/stack trace.

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.

Ferret7446 · 7 months ago
Stack traces are expensive. You need to make a conscious decision whether you want a stack trace attached to a particular error (and possibly where it gets attached, if you want it higher in the call chain), which aligns with Go's design philosophy for error handling.
theamk · 7 months ago
C++-style stack traces, with stack walking and ELF section analysis, are expensive. Python's stack traces, which also involve reading source files from disk, are expensive as well.

But go's do not have to be. A compiler can expand "foo()?" to something like:

err := foo(); if err != nil { return err.WithStringContext("foo() in MyFile.go:25"); }

The only complexity there is appending a constant pointer to "err", and this only happens in error case that uses "?". Depending on implementation it could be a single word write, if compiler can prove there are no other users of "err".

(And if your code is carefully written to be allocation-free and appending a pointer kills that? In this case, you don't have to use "?", put "return err" directly.)

sigmonsays · 7 months ago
I vote no on this proposal.

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.

echelon · 7 months ago
> These are all tools, just pick the one you like and stop trying to make them like others.

I'm not so sure I agree with that. I'm glad Rust continues to evolve at a healthy pace and pick up new syntactic features.

Boilerplate is a sin against the brain. As long as you don't pay it down with increased cognitive complexity, it should be eliminated at all costs.

Error handling is such an essential feature of problem solving that this should be one of the highest priorities for a language to get right. It should be so simple and elegant that you can bash it out in a few tokens, and almost impossible to get wrong.

I think Go would be a much better language if it took this to task. I reach for Rust for out of domain problems because it's so expressive and intentional and safe and concise.

eweise · 7 months ago
By that logic, Go shouldn't have generics but they've added a lot of value to our codebase.
nonethewiser · 7 months ago
Go is an incomplete language masquerading as a simple one.
pjmlp · 7 months ago
C should not care for safety, there are tools, pick the one you like....
RedNifre · 7 months ago
(I'm not a Go programmer)

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) }

pornel · 7 months ago
The ? syntax agrees that errors should just be regular values returned from functions, and handling of errors should be locally explicit. It's not a different approach from `if err != nil return err`, it merely codifies the existing practice, and makes expressing the most common cases more convenient and clearer.

It's clearer because when you see ? you know it's returning the error in the standard way, and it can't be some subtly different variation (like checking err, but returning err2 or a non-nil ok value). The code around it also becomes clearer, because you can see the happy path that isn't chopped up by error branches, so you get high signal to noise ratio, fewer variables in the scope, without losing the error handling.

RedNifre · 7 months ago
I think what I meant is that out of these three ways to do it, I don't like the second one:

1. The language has the feature of returning multiple values, which is then used in an error handling pattern.

2. The proposal is about special syntax for that pattern

3. Languages that have generics (like Go) can instead implement a Result type, turning this pattern into more readable code without the extra syntax.

I feel like a Result type would have more advantages and be less disruptive than a syntax macro for a pattern, but I'm not sure.

cratermoon · 7 months ago
This is from Ian Lance Taylor, a major figure in the development of Go. Taylor was instrumental in bringing generics to the language, this proposal is worth taking seriously.
9rx · 7 months ago
> Taylor was instrumental in bringing generics to the language, this proposal is worth taking seriously.

He submitted, what, 8 failed generics proposals before Phil Wadler came in to figure out what he was missing?

I don't mean to diminish what he has done. He is clearly an important contributor and even those failed proposals were important steps along the way. What I do mean is that judging a proposal based on who it is written by is silly. Imagine if one of those early generics proposals were taken seriously just because of who he is. I expect even he would be unhappy about that outcome in hindsight.

The author is irrelevant. If it is a good proposal, it can stand on its own merits.

cratermoon · 7 months ago
Thanks for your insights, they are both penetrating and diffuse.
jpalawaga · 7 months ago
It's worth noting that 'taking a proposal seriously' doesn't equate to accepting it.
827a · 7 months ago
I overall like it and would prefer a world where Go had this spec implemented versus did not.

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.

bpt3 · 7 months ago
I don't care for this spec and probably wouldn't use it if it were implemented, but I do like your suggestion of how to handle err shadowing.
zimbatm · 7 months ago
This change would mark a turn in the language's evolution, as it would be the first implicit variable to be added to the language.

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.

pjmlp · 7 months ago
It is not more explicit than C, Pascal, JOVIAL and many other predating it for decades.
zemo · 7 months ago
there's some precedent in the direction of adding predeclared identifiers for error handling: the identifier `error` was originally not predeclared, you had to import it from `io` (or maybe `os`?) and refer to it as `io.Error` everywhere.