Hacker News new | past | comments | ask | show | jobs | submit login
Discussion: Reduce error handling boilerplate in Golang using '?' (github.com/golang)
48 points by omani 10 days ago | hide | past | favorite | 91 comments





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


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.

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.

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.

  foo else { bar }

How about this? :)

    { foo } catch { bar }

This is probably the one that would anger Golangers the most lol

They could even have typed error checking for different errors and an automatic return error syntax.

    { Foo }
    catch (err) {}
    catch (err2){}
    throw

Agreed. The reduction in boilerplate to route error conditions is what we really need, if we are going to add any special error concept. errors.Is/errors.As and the accompanying switch/if statements can quickly consume entire screens of code for all but the most simplest of failure cases and as a result it often sees developers become lazy and just return err instead.

It could be:

foo ?! { bar }

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


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.


V, which is syntactically similar to Go, uses

  foo or { bar }

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.


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.


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?


Error patterns aren't type checked in Go. So you can forget to check an error, not notice, and ship a bug. It compounds with features like zero values and pointers-as-null to make these bugs either subtle or not subtle, but discoverable only at runtime.

> So you can forget to check an error, not notice, and ship a bug.

How would you forget, exactly? Your tests are going to blow in your face should you ever leave out an entire feature from the implementation.

If you forgot to document that feature in your tests, which is more likely, then you've created a situation of undefined behaviour, not a bug. You've made no claims as to what should happen. All possible behaviours are equally valid.

And no, pattern matching doesn't help you here as you still need to document what happens on those pattern matches. If you forget to document these cases you've still got the very same undefined behaviour. There is no escaping the need for the tests.

It's cool that there is nicer syntax, that your editor can warn you of mistakes while you are typing, and that can save time and all that don't get me wrong, but this idea that you are going straight up forget without any notice of your forgetfulness, as fun as a trope as it is, just isn't realistic.


> How would you forget, exactly? Your tests are going to blow in your face should you ever leave out an entire feature from the implementation.

> ... this idea that you are going straight up forget without any notice of your forgetfulness, as fun as a trope as it is, just isn't realistic.

By forgetting. Copying `if err != nil` is a mundane and repetitive process, it's easy for your brain to go into auto-pilot.

> And no, pattern matching doesn't help you here as you still need to document what happens on those pattern matches. If you forget to document these cases you've still got the very same undefined behaviour. There is no escaping the need for the tests.

Pattern matching (with algebraic data types/enum/unions) helps because it forces you to check the error. It becomes impossible to use a return value without checking the error.


> By forgetting.

Like in the same way you might forget to write pattern matching code? I mean, that's possible, but the checks and balances are going to let you know. In light of that, what is the significance of forgetting for the few seconds, if that, before getting notified of your forgetfulness? That's not a real problem.

> Pattern matching (with algebraic data types/enum/unions) helps because it forces you to check the error.

Checking the error alone is pointless. You need to also do something with the error, and pattern matching does nothing to help you with that. But that's what tests are for, there to help you with exactly that.

And since your code needs the right branching strategy to get to the point of doing something with the error as validated against the documentation, you also know that your branches are present and working as documented. You cannot possibly forget them after applying the checks and balances. How could you?

All you can really forget to do, maybe, is to document what the program is supposed to do. But in that case the program isn't supposed to do what you forgot to add anyway. Anything missed is undefined behaviour. If you have forgotten to consider what you want your program to do, no language can help you with that!


In Go you can forget to call a function with the correct arguments, and it will not compile, right?

No, definitely not. The compiler couldn't care less if your arguments are correct. Consider:

   func cleanupTemporaryDir() {
       os.RemoveAll("/")
   }
You might have some very unhappy users if you ship that. Of course, as you would have documented your intended behaviour, the mistake is going to get caught when you run your tests, making it abundantly clear that you screwed up before bad things happen. Like before, you're not going to end up out.

Sure, a language with a deeper type system can place restrictions on what kinds of values you can pass in. "/" would be prevented by the type checker, letting it be known as you type and not later when you run the tests. That is nice. Nobody is going to deny that. But this act as if it is the only thing that will save you is completely disconnected from reality. Our world does not exist in this imagined vacuum where when one thing doesn't exist nothing does.


> Like in the same way you might forget to write pattern matching code?

This is a lengthy response to seemingly ignore, or miss, the point being made.

It is literally impossible to "forget" if you have pattern matching + algebraic data types/enum/unions. Conversely, it is possible and easy with Go.

Nothing you have said acknowledges this.


> It is literally impossible to "forget" if you have pattern matching + algebraic data types/enum/unions.

No, it's entirely possible to forget. That should be obvious. It is a functional necessity for you to be able to forget as you can only type so much at a time. You cannot possibly write code to call a function and perform pattern matching on the result at the exact same moment in time. There fundamentally has to be a period of time where the work will be incomplete, which opens the opportunity to not finish. It should be abundantly apparent to you that that you can forget to finish what you started in such a scenario.

Your checks and balances will alert you of your forgetfulness, but that's true of all languages. The biggest risk is that you will forget to define behaviour. But if you fail to do that, you've got problems even in languages with pattern matching and associated types. ADTs/enums/unions/pattern matching. are not sufficient to define behaviour. Not even close.

Like you said, pattern matching only helps you with checking errors. But there is no reason to check errors in the first place if you don't do something with the error, and for that you need to document what the error condition means for the user. Otherwise you have undefined behaviour. And once you've documented the behaviour, the error checks are confirmed for free anyway. You can't forget. The checks and balances will make it known, spectacularly.

What, exactly, are you trying to accomplish by overselling what is already a fantastic feature by its own merits on the basis of something that isn't at all realistic? If you want to get developers excited about the feature, there are way more compelling attributes to extol!

I have read this exact claim before, for what it is worth. Did you end up here simply because you repeated what you saw elsewhere without actually thinking about it?


The distinction you're not acknowledging is that with pattern matching + algebraic data types you can't do anything with the payload without handling the error case. (You need to write a pattern match to extract the value, and the match needs to be exhaustive.)

With a separate error value, like in Go, that's no longer the case. The code for handling the error and the code for doing something with the payload are totally separate, and don't both need to be present.


What is to be acknowledge, exactly? By Go convention, values are to always be "useful", so the error is only significant if you need to use the error value for some reason. They are not logically intertwined like you have postulated.

Consider:

   func GetData() (*Data, error)
Both returned values are independently observable. Unless you need the error value for your particular situation, the Data variable contains everything you need to know. You can otherwise ignore the error value. Likewise, if all you need is the error value, you can ignore the Data value.

Success/failure values are logically intertwined by convention in some other languages, but you can't reasonably take idioms from other languages and slap them down on Go like that. Just as you cannot reasonably do so in reverse. Different languages express things differently. That's what makes them different.

Is that the disconnect here? That you believe all languages share the exact same idioms?


No, all languages don't need to share the same idioms.

But you've been making arguments like the following:

> Like in the same way you might forget to write pattern matching code?

> No, it's entirely possible to forget. That should be obvious. It is a functional necessity for you to be able to forget as you can only type so much at a time. You cannot possibly write code to call a function and perform pattern matching on the result at the exact same moment in time.

And that is just disingenous. That's not you having a different opinion on something that's a matter of taste. That's you repeatedly trying to suggest that both forms of error handling are equally likely to suffer from somebody accidentally failing to handle the error.

And obviously that's not true, exactly because the returned values are logically separate in Go and aren't in the other lanuages. That is, in typical Go code, if you assign the error to err but omit the if err != nil {} boilerplate, the code will typically compile. (It'll compile, because the error variable is basically always named err, and most functions will have other references to that variable, such that the "unused variable" compiler diagnostics don't trigger.)


> the code will typically compile.

It may compile, but won't pass the test. Are you under the impression that a developer suddenly becomes blind after compilation and somehow magically won't see that the test failed? That doesn't happen. How are you going to forget? In reality, your forgetfulness is going to be made loud and clear. –– We offered a challenge to another account. He failed miserably, as did I, but maybe you can do better? https://news.ycombinator.com/item?id=42865349

And, before you even think it, no, you cannot skip that test in another language with pattern matching. There is nothing pattern matching can do to help you with what it is testing. It should be obvious to you that even if you do successfully pattern match, you might, for example, "forget" to return the right value. The documentation and additional checks and balances are required either way. The type system does not save you here.

And, before you even think it, no, "but what if I am a dummy who doesn't understand his tools or how to program???" doesn't work either as if you are that dummy then you aren't going to use the right types to activate the pattern matching semantics of which we speak, so pattern matching is not going to notice anyway. There is a necessary assumption in this discussion that you understand the tools, idioms, and software engineering in general. We cannot even meaningfully talk about pattern matching alone without that assumption.

So, how are you going to forget, exactly? You won't. This isn't a real thing. Pattern matching is cool, though, with many real benefits. It is funny that someone decided to focus on a made up benefit instead of what makes it actually great. But this is a commonly repeated trope so I'm sure we got here because he blindly repeated it without thinking about it for even a millisecond.


> I have read this exact claim before, for what it is worth. Did you end up here simply because you repeated what you saw elsewhere without actually thinking about it?

I can't tell if you're being intentionally obtuse. It behooves you to research the topic being discussed, instead of writing long-winded snarky posts.

It is not worth continuing this discussion unless you can demonstrate, with explicit code examples, what you think we're talking about, and why you believe it is incorrect.


Okay, sure. Here is an explicit code example:

     func foo(bar func() error) bool {
          err := bar()
          if err != nil {
               return true
          }
          return false
     }

     func TestFooReturnsTrueWhenBarReturnsError(t *testing.T) {
          if foo(func() error { return nil }) {
               t.Error("expected foo to return false")
          }
          if !foo(func() error { return errors.New("error") }) {
               t.Error("expected foo to return true")
          }
     }
Now it is your turn. Modify the body of function foo to remove the error check as you imagine it would be if it were forgotten.

This https://go.dev/play/p/k8zDQj5knaj is what I envision you are talking about, but clearly that cannot be it. As you can see it loudly proclaims that I "forgot", making this forgetfulness idea you have come up with impossible. So, what it is that you actually have in mind?


That is not what is being discussed and you know it.

Provide an example backing up your claim regarding pattern matching + union types:

> Like in the same way you might forget to write pattern matching code?


> That is not what is being discussed and you know it.

That is exactly what is being discussed. Why would the topic arbitrarily change? In case you have forgotten, my literal question/assertion was:

"How would you forget, exactly? Your tests are going to blow in your face should you ever leave out an entire feature from the implementation."

Go on, show us: How would you forget?

> Provide an example backing up your claim regarding pattern matching + union types:

Uh... Okay. Sure? Strange request, but here is an example in CrabLang that meets your ask:

    let result = foo();
    match result {
        Ok(value) => println!("Result: {}", value),
    }
There you go. I forgot to check the error.

Like I said in the full comment that you quoted from, you'll soon recognize your forgetfulness once you apply your checks and balances, but for that brief moment in between it is possible to forget. It must be that way else it would be impossible to input the program. If that's what you think we're talking about... Why? And let me ask again like I did in the comment you quoted from: What is the significance of that? If you had carried that quote through you'd also see: "That's not a real problem." I stand by that. Why not you?


I'm not sure what the other commenter's point is. If you're checking if the error is nil, or pattern matching on the result of a call, it feels like exactly the same.

In your CrabLang example, you don't have warnings on (I assume they exist) for completion or whatever. There's still the Err case that should at least throw a warning for not being caught.


You "can" forget to check an error, but in practice I've never seen it happen. The error check always happens after it is assigned so a missing check stands out like a clown at a funeral, and as a bonus there are linters that check for missed errors, which is trivial due to how Go and Go errors work.

100% this. The idea is ok, the tooling to actually use it is terrible

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


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


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

This isn't foolproof. If you're calling multiple methods and reusing `err` it won't give an error because it's technically not unused.


I didn't know that, this seems like a big foot gun tbh

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


Traditionally linters are workarounds for features that should be in the language.

Instead we pack best practices in an external tool.


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


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.


As I have pointed in in my other comment, 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....:

Even less pretty for getattr. And then you have a rude awakening about the difference between the missing default behavior of these two functions.

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

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.

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.

Big same.

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.

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.

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


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.

So hand rolled call stack traces? I just don’t understand why this is better than exceptions.

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.

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.


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.

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


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.


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


By that logic, Go shouldn't have generics but they've added a lot of value to our codebase.

Go is an incomplete language masquerading as a simple one.

C should not care for safety, there are tools, pick the one you like....

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


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.


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.


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.

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.

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.

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


Thanks for your insights, they are both penetrating and diffuse.

It's worth noting that 'taking a proposal seriously' doesn't equate to accepting it.

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.


It is not more explicit than C, Pascal, JOVIAL and many other predating it for decades.

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.

Unfortunately, every software project will eventually reach a point of maturity where more and more features are added simply for the sake of adding them.

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


What is happening is that Go designers are discovering that the "academic" features of the language that predated it for decades have gotten them for a reason.

My knee jerk reaction is that introducing even more ways to write the same thing is going to slowly bloat the language, but Go does it infrequently enough that it doesn't seem like it's going to become a huge problem. I think I could get used to this syntax.

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


I really hate that a bare ? makes us lose some info on code coverage. If you only test the happy path, the line is counted as covered. Making the return explicit at least makes it obvious when a line is uncovered.

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.


It is crazy that error handling is one of the most important things, yet even modern languages suck at it.

In my opinion everything should return type like MayFail<T>, Result<T>


The proposal confuses shadowing of err, which is mostly irrelevant anyway but at least you can see it today. It also makes breakpoints or inserting log statements etc. more difficult without reformatting code. And lastly, it teases the ternary conditional operator that Go lacks, constantly reminding me of this. So IMO I would use a different language rather than adopt this.

Error handling is really not an issue that needs fixing in Golang. That being said, I wish Golang had an assert key word as a shortcut to "if cond { panic }". A lot of those "if err != nil" in the wild should really just be assertions.

There are 0 times I’d want to panic in go code in production services.

Seems like the author assumes that only one error is returned. What if I want to return []error? What happens if my return objects are out of the normal order? Like F() (int, error, int) {...}.

The proposal is nice, but a bit shallow.


> What happens if my return objects are out of the normal order? Like F() (int, error, int) {...}

The convention is that error should be the last return value. If the error is not nil, then discard other returned values.


If I may make a suggestion to @ianlancetaylor, I think using the ? for error checking is a fantastic idea, but I think a couple small changes would make this absolutely a game changer and even more Go-like:

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.


honestly with copilot and friends this is not really an issue

This breaks go's readability and explicit nature. No thanks. The author doesn't understand the implications of the proposal. What if the args are in a different order returned? "foo, error" or "error, *foo" ? - I've seen many permutations of this. Being explicit about error handling is actually a good thing.

For clarity: The author is Ian Lance Taylor. He's a principal engineer at Google, on the Golang team, and the 4th largest contributor to the language. The personal attack on him not understanding the implications of the proposal is a bit cringe.

I know who he is, I stand by my statement that the proposal is ill-advised.

5 years ago I would be more sympathetic to this proposal.

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.


The boilerplate does not go away just because an LLM can write it. It still makes the code harder to read.



Consider applying for YC's Spring batch! Applications are open till Feb 11.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: