(On | No) Syntactic Support for Error Handling

338 henrikhorluck 445 6/3/2025, 4:18:45 PM go.dev ↗

Comments (445)

threemux · 14h ago
If you feel the need (as many have in this thread) to breezily propose something the Go Team could have done instead, I urge you to click the link in the article to the wiki page for this:

https://go.dev/wiki/Go2ErrorHandlingFeedback

or the GitHub issue search: https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aer...

I promise that you are not the first to propose whatever you're proposing, and often it was considered in great depth. I appreciate this honest approach from the Go Team and I continue to enjoy using Go every day at work.

hackingonempty · 13h ago
The draft design document that all of the feedback is based on mentions C++, Rust, and Swift. In the extensive feedback document you link above I could not find mention of do-notation/for-comprehensions/monadic-let as used Haskell/Scala/OCaml. I didn't find anything like that in the first few pages of the most commented GitHub issues.

You make it out like the Go Team are programming language design wizards and people here are breezily proposing solutions that they must have considered but lets not forget that the Go team made the same blunder made by Java (static typing with no parametric polymorphism) which lies at the root of this error handling problem, to which they are throwing up their hands and not fixing.

munificent · 9h ago
I think Go should have shipped with generics from day one as well.

But you breezily claiming they made the same blunder as Java omits the fact that they didn't make the same blunder as Rust and Swift and end up with nightmarish compile times because of their type system.

Almost every language feature has difficult trade-offs. They considered iteration time a priority one feature and designed the language as such. It's very easy for someone looking at a language on paper to undervalue that feature but when you sit down and talk to users or watch them work, you realize that a fast feedback loop makes them more productive than almost any brilliant type system feature you can imagine.

hackingonempty · 7h ago
This is a very good point, fast compilation times are a huge benefit. The slow compiler is a downside of languages like Rust, Scala, and Haskell. Especially if you have many millions of lines of code to compile like Google.

However, OCaml has a very fast compiler, comparable in speed to Go. So a more expressive type system is not necessarily leading to long compilation times.

Furthermore, Scala and Haskell incremental type checking is faster than full compilation and fast enough for interactive use. I would love to see some evidence that Golang devs are actually more productive than Scala or Haskell devs. So many variables probably influence dev productivity and controlling for them while doing a sufficiently powered experiment is very expensive.

umanwizard · 8h ago
What makes you think Rust’s compile times are related to its type system?
munificent · 5h ago
The way the type system interacts with the rest of the language leads you down the path to monomorphization as the compilation strategy. Monomorphizing is what gives you huge piles of instantiated code that then has to be run through the compiler back end.

Blaming it on LLVM like another comment does misses the point. Any back end is slow if you throw a truck-load of code at it.

I'm not saying monomorphization is intrinsically bad. (My current hobby language works this way.) But it's certainly a trade-off with real costs and the Go folks didn't want their users to have to pay those costs.

Lvl999Noob · 3h ago
Monomorphization has got nothing to do with type system though. If you have a GC (as go does), you can automatically box your references and go from a `impl Trait` to a `&mut dyn Trait` with the GC taking care of value vs reference semantics. Monomorphization is orthogonal to how you define the set of valid arguments.
littlestymaar · 3h ago
You realize that having a generics and having monomorphization are two orthogonal things, right?

If you're not aiming for the highest possible performance, you can type erase your generics and avoid the monomorphization bloat. Rust couldn't because they wanted to compete with C++, but Go definitely could have.

Yoric · 8h ago
Last time I checked, Rust's slow compile times were due to LLVM. In fact, if you want to make Rust faster to compile, you can compile it to wasm using cranelift.
littlestymaar · 3h ago
Not just LLVM in itself but the Front-end codegen: AFAIK the rust front-end emits way too much LLVM IR and then counts on LLVM to optimize and they have been slowly adding optimizations inside the front-end itself to avoid IR bloat.

And there's also the proc macro story (almost every project must compile proc_macro2 quote and syn before the actual project compilation even starts).

littlestymaar · 3h ago
This has been a lazy excuse/talking point from the Go team for a while, but in realitiy Generics aren't the reason why Rust and Swift compile slowly, as can be easily shown by running cargo check on a project using a hefty dose of generics but without procedural macros.
9rx · 13h ago
> lets not forget that the Go team made the same blunder made by Java

To be fair, they were working on parametric polymorphism since the beginning. There are countless public proposals, and many more that never made it beyond the walls of Google.

Problem was that they struggled to find a design that didn't make the same blunder as Java. I'm sure it would have been easy to add Java-style generics early on, but... yikes. Even the Java team themselves warned the Go team to not make that mistake.

throwaway2037 · 4h ago

    > blunder made by Java
For normies, what is wrong with Java generics? (Do the same complaints apply to C# generics?) I came from C++ to Java, and I found Java generics pretty easy to use. I'm not interested in what "PL (programming language) people" have to say about it. They dislike all generic/parametric polymorphism implementations except their pet language that no one uses. I'm interested in practical things that work and are easy for normies to learn and use well.

    > Even the Java team themselves warned the Go team to not make that mistake.
Do you have a source for that?
cayley_graph · 1h ago
> I'm not interested in what "PL (programming language) people" have to say about it. They dislike all generic/parametric polymorphism implementations except their pet language that no one uses.

That's strange. I seem to recall the PL community invented the generics system for Java [0,1]. Actually, I'm pretty sure Philip Wadler had to show them how to work out contravariance correctly. And topically to this thread, Rob Pike asked for his help again designing the generics system for Go [2,3]. A number of mistakes under consideration were curtailed as a result, detailed in that LWN article.

There are countless other examples, so can you elaborate on what you're talking about? Because essentially all meaningful progress on programming languages (yes, including the ones you use) was achieved, or at least fundamentally enabled, by "PL people".

[0] https://homepages.inf.ed.ac.uk/wadler/gj/

[1] https://homepages.inf.ed.ac.uk/wadler/gj/Documents/gj-oopsla...

[2] https://arxiv.org/pdf/2005.11710

[3] https://lwn.net/Articles/824716/

moomin · 2h ago
Yeah, it _doesn’t_ apply to C# generics. Basically, if you’ve got List<Person> and List<Company> in C#, those are different classes. In Java, there’s only one class that’s polymorphic. This causes a surprising number of restrictions: https://docs.oracle.com/javase/tutorial/java/generics/restri...
tpm · 1h ago
Type erasure is what is wrong with Java generics. It causes many issues downstream.
karmakaze · 12h ago
At least Java supports covariance and contravariance where Go only supports invariant generics.
PaulHoule · 12h ago
Java has evolved to contain much of “ML the good parts” such as that languages like Kotlin or Scala that offer a chance to be just a bit better in the JVM look less necessary
throwaway2037 · 4h ago

    > Java has evolved to contain much of “ML the good parts”
Can you give some examples?
yz453 · 3h ago
Not OP. IMO the recent Java changes, including pattern matching (especially when using along with sealed interface), virtual threads (and structured concurrency on the way), string templates, are all very solid additions to the language.

Using these new features one can write very expressive modern code while still being interoperable with the Java 8 dependency someone at their company wrote 20 years ago.

platinumrad · 11h ago
Even Rust and F#[1] don't have (generalized) do notation, what makes it remotely relevant to a decidedly non-ML-esque language like Go?

[1] Okay fine, you can fake it with enough SRTPs, but Don Syme will come and burn your house down.

nine_k · 9h ago
IDK, Python was fine grabbing list comprehensions from Haskell, yield and coroutines from, say, Modula-2, the walrus operator from, say, C, large swaths from Smalltalk, etc. It does not matter if the languages are related; what matters is whether you can make a feature / approach fit the rest of the language.
platinumrad · 1h ago
Generalized do notation as GP is proposing requires HKT. I don't think it's controversial to say that Go will not be getting HKT.
ablob · 1h ago
Afaik the F# supports do notation through computation expressions.
platinumrad · 1h ago
Like Rust, F# doesn't have higher-kinded types so it's not generalized like GP is proposing. Each type of computation expression is tied to a specific monad/applicative.
evntdrvn · 10h ago
hahaha :D
yusina · 12h ago
It fascinates me that really smart and experienced people have written that page and debated approaches for many years, and yet nowhere on that page is the Haskell-solution mentioned, which is the Maybe and Either monads, including their do-notation using the bind operator. Sounds fancy, intimidating even, but is a very elegant and functionally pure way of just propagating an error to where it can be handled, at the same time ensuring it's not forgotten.

This is so entrenched into everybody writing Haskell code, that I really can't comprehend why that was not considered. Surely there must be somebody in the Go community knowing about it and perhaps appreciating it as well? Even if we leave out everybody too intimidated by the supposed academic-ness of Haskell and even avoiding any religios arguments.

I really appreciate the link to this page, and overall its existence, but this really leaves me confused how people caring so much about their language can skip over such well-established solutions.

jchw · 12h ago
I don't get why people keep thinking it was forgotten; I will just charitably assume that people saying this just don't have much background on the Go programming language. The reason why is because implementing that in any reasonable fashion would require massive changes to the language. For example, you can't build Either/Maybe in Go (well, of course you can with some hackiness, but it won't really achieve the same thing) in the first place, and I doubt hacking it in as a magic type that does stuff that can't be done elsewhere is something the Go developers would want to do (any more than they already have to, anyway.)

Am I missing something? Is this really a good idea for a language that can't express monads naturally?

yusina · 10h ago
> I don't get why people keep thinking it was forgotten

Well, I replied to a post that gave a link to a document that supposedly exhaustively (?) listed all alternatives that were considered. Monads are not on that list. From that, it's easy to come to the conclusion that it was not considered, aka forgotten.

If it was not forgotten, then why is it not on the list?

> Is this really a good idea for a language that can't express monads naturally?

That's a separate question from asking why people think that it wasn't considered. An interesting one though. To an experienced Haskell programmer, it would be worth asking why not take the leap and make it easy to express monads naturally. Solving the error handling case elegantly would just be one side effect that you get out of it. There are many other benefits, but I don't want to make this into a Haskell tutorial.

jchw · 10h ago
It's not an exhaustive list of every possible way to handle errors, but it is definitely, IMO, roughly an exhaustive list of possible ways Go could reasonably add new error handling tools in the frame of what they already have. The reason why monads and do notation don't show up is because if you try to write such a proposal it very quickly becomes apparent that you couldn't really add it to the Go programming language without other, much bigger language change proposals (seriously, try it if you don't believe me.) And for what it's worth, I'm not saying they shouldn't, it's just that you're taking away the wrong thing; I am absolutely 100% certain this has come up (in fact I think it came up relatively early in one of the GitHub issues), but it hasn't survived into a proposal for a good reason. If you want this, I believe you can't start with error handling first; sum types would probably be a better place to start.

> That's a separate question from asking why people think that it wasn't considered. An interesting one though. To an experienced Haskell programmer, it would be worth asking why not take the leap and make it easy to express monads naturally. Solving the error handling case elegantly would just be one side effect that you get out of it. There are many other benefits, but I don't want to make this into a Haskell tutorial.

Hmm, but you could say that for any idea that sounds good. Why not add a borrow checker into Go while we're at it, and GADTs, and...

Being blunt, this is just incorrect framing. Concepts like monads and do notation are not inherently "good" or "bad", and neither is a language feature like a borrow checker (which also does not mean you won't miss it when it's not there in languages like Go, either). Out of context, you can't judge whether it's a good idea or not. In context, we're talking about the Go programming language, which is not a blank slate for programming language design, it's a pre-existing language with extremely different values from Haskell. It has a pre-existing ecosystem built on this. Go prioritizes simplicity of the language and pragmatism over expressiveness and rich features nearly every time. This is not everyone's favorite tradeoff, but also, programming language design is not a popularity contest, nor is it an endeavor of mathematical elegance. Designers have goals, often of practical interest, that require trade-offs that by definition not everyone will like. You can't just pretend this constraint doesn't exist or isn't important. (And yes we know, Rob Pike said once in 2012 that Go was for idiots that can't understand a brilliant language. If anyone is coming here to make sure to reply that under each comment as usual on HN, consider it pre-empted.)

So to answer the question, would it be worth the leap to make it easy to express monads naturally in Go? Obviously, this is a matter of opinion and not fact, but I think this is well beyond the threshold where there is room for ambiguity: No. It just does not mesh with it at all, does not match nearly any other decision made anywhere else with regards to syntax and language features, and just generally would feel utterly out of place.

A less general version of this question might be, "OK: how about just sum types and go from there?"—you could probably add sum types and express stuff like Maybe/Either/etc. and add language constructs on top of this, but even that would be a pretty extreme departure and basically constitute a totally new, distinct programming language. Personally, I think there's only one way to look at this: either Go should've had this and the language is basically doomed to always have this flaw, or there is room in the space of programming languages for a language that doesn't do this without being strictly worse than languages that do (and I'm thinking here in terms of not just elegance or expressiveness but of the second, third, forth, fifth... order effects of such a language design choice, which become increasingly counter-intuitive as you follow the chain.)

And after all, doesn't this have to be the case? If Haskell is the correct language design, then we already have it and would be better off writing Haskell code and working on the GHC. This is not a sarcastic remark: I don't rule out such dramatic possibilities that some programming languages might just wind up being "right" and win out in the long term. That said, if the winner is going to be Haskell or a derivative of it, I can only imagine it will be a while before that future comes to fruition. A long while...

Yoric · 8h ago
Well, Rust's `?` was initially designed as a hardcoded/poor man's `Either` monad. They quote `?` as being one of the proposals they consider, so I think that counts?

Source: I'm one of the people who designed it.

joaohaas · 9h ago
It was not forgotten. Maybe/Either and 'do-notation' are literally what Rust does with Option/Result and '?', and that is mentioned a lot.

That said as mentioned in a lot of places, changing errors to be sum types is not the approach they're looking for, since it would create a split between APIs across the ecosystem.

dcow · 8h ago
Where there’s a will there’s a way. Swift is almost universally compatible with objective-c and they are two entirely different languages no less. If an objective-c function has a trailing *error parameter, you can, in swift, call that function using try notation and catch and handle errors idiomatically. All it takes is for one pattern to be consistently expressible by another. Why can’t Result/Either types be api-compatible with functions that return tuples?
Yoric · 8h ago
Indeed, I can testify that `?` was very much designed with the do-notation in mind.
anentropic · 13h ago
It's probably already answered somewhere, but I am curious why it's such a problem in Go specifically, when nearly every language has something better - various different approaches ... is the problem just not being able to decide / please everyone, or there's something specific about Go the language that means everyone else's solutions don't work somehow?
mamcx · 6h ago
> is the problem just not being able to decide / please everyone,

Reading this article? in fact yes(?):

> After so many years of trying, with three full-fledged proposals by the Go team and literally hundreds (!) of community proposals, most of them variations on a theme, all of which failed to attract sufficient (let alone overwhelming) support, the question we now face is: how to proceed? Should we proceed at all?

> We think not.

This is a problem of the go designers, in the sense that are not capable to accept the solutions that are viable because none are total to their ideals.

And never will find one.

____

I have use more than 20 langs and even try to build one and is correct that this is a real unsolved problem, where your best option is to pick one way and accept that it will optimize for some cases at huge cost when you divert.

But is know that the current way of Go (that is a insignificant improvement over the C way) sucks and ANY of the other ways are truly better (to the point that I think go is the only lunatic in town that take this path!), but none will be perfect for all the scenarios.

ok_dad · 13h ago
Go has specific goals like not hiding control flow. This would go against those goals, at least the ways people have thought to do it so far.
LinXitoW · 8h ago
Isn't defer hidden control flow? The defer handling can happen at any point in the function, depending on when errors happen. Exactly like a finally block.
tialaramex · 13h ago
I don't see how Try (the ? operator) is hidden control flow. It's terse, but it's not hidden.
9rx · 12h ago
Even the laziest programmer is going to want to wrap the error with another error, and usually you will want to do more than that.

You can put that in-band, with something like:

    v := funcWithError()? err := { 
        return fmt.Errorf("lazy programmer wrapping: %w", err)
    }
But in that case what have you really gained?

Or you can do some kind of magic to allow it to happen out of band:

    // found somewhere else in the codebase
    func wrapErr(err error) error {
        if err == nil {
            return nil
        }
        return fmt.Errorf("lazy programmer wrapping: %w", err)
    }

    v := funcWithError()?(wrapErr)
        
But that's where you start to see things hidden.
ok_dad · 12h ago
I personally agree, but I’m not the go team. The hidden control flow was specifically called out but about the try keyword. I like the ? and similar ways of checking nulls, but personally I don’t mind the verbosity in go, even if there are footguns.
jchw · 12h ago
IMO: because it behaves like structured control flow (i.e. there is a branch) but it doesn't look like structured control flow (i.e. it doesn't look like there is a branch; no curly braces). I don't think there's a single other case in the Go programming language: it doesn't even have the conditional ternary operator, for example.
_kidlike · 3h ago
the obvious solution is try-catch, Java style. Which I'm surprised it's not even mentioned in the article. Not even when listing cons that wouldn't have been there with try-catch.

But of course that would hurt them and the community in so many levels that they don't want to admit...

jchw · 2h ago
I strongly do not like try/catch. Just to list the limitations of exceptions that come to mind,

- try/catch exceptions obscure what things can throw errors. Just looking at a function body, you can't see what parts of the functions could throw errors.

- Relatedly, try/catch exceptions can unwind multiple stack frames at once, sometimes creating tricky, obscure control flow. Stack unwinding can be useful, especially if you really do want to traverse an arbitrary number of stack frames (e.g. to pass an error up in a parser or interpreter, or for error cases you really don't want to think about handling as part of the normal code flow) but it's tricky enough that it's undesirable for ordinary error handling.

- I think most errors, like I/O errors, are fairly normal occurrences, i.e. all code should be written with handling I/O errors in mind; this is not a good use case for this type of error handling mechanism—you might want to pass the error up the stack, but it's useful to be confronted with that decision each time! With exceptions, it might be hard to even know whether a given function call might throw an I/O error. Function calls that are fallible are not distinguishable from function calls that are infallible.

- This is also a downside of Go's current error handling; with try/catch exceptions you can't usually tell what exceptions a function could throw. (Java has checked exceptions, but everyone hates them. The same problem doesn't happen for enum error types in Rust Result, people generally like this.)

(...But that's certainly not all.)

Speaking just in terms of language design, I feel that Rust Result, C++ std::expected, etc. are all going in the right direction. Even Go just having errors be regular values is still better in my opinion.

(Still, traditional exceptions have been proposed too, of course, but it wasn't a mistake to not have exceptions in Go, it was intentional.)

LoganDark · 11h ago
`return` doesn't have braces either.
jchw · 10h ago
Closest thing to a real interblock branch without braces, IMO, is `break` and `continue`, but those are both at least lone statements, not expressions. It "looks like" control flow. Personally, I don't count `return`, I view it as it's own thing from a logical standpoint. Obviously if we were talking about literal CPU doing a jump, well then a lot of things would count, but that's not what I mean in the frame of structured control flow and more in the realm of implementation details.
kiitos · 11h ago
Currently if you want to return from a function/method you need to type `return` in the source code. And return is a single expr, it can't be chained or embedded, and in almost all cases it exists on its own line in the file. This is an important invariant for Go, even if you don't understand or buy its motivation. `?` would fundamentally change that core property of control flow. In short, chaining is not considered to be a virtue, and is not something that's desired.
skywhopper · 9h ago
That only covers one tiny case among several possible error flows. Why add special syntax for that?
skywhopper · 9h ago
The thing is, it’s not actually a major problem. It’s the thing that gets the most complaints for sure, and rubs folks from other languages the wrong way often. But it’s an intentional design that is aware of its tradeoffs. As a 10 year Go veteran, I strongly prefer Go’s approach to most other languages. Implicit control flow is a nightmare that is best avoided, imo.

It’s okay for Go to be different than other languages. For folks who can’t stand it, there are lots of other options. As it is, Go is massively successful and most active Go programmers don’t mind the error handling situation. The complaints are mostly from folks who didn’t choose it themselves or don’t even actually use it.

The fact that this is the biggest complaint about Go proves to me the language is pretty darn incredible.

munificent · 9h ago
I think the two big things for Go are:

1. Minimalism.

Go has always had an ethos of extreme minimalism and have deliberately cultivated an ecosystem and userbase that also places a premium on that. Whereas, say, the Perl ecosystem would be delighted to have the language add one or seven knew ways of solving the same problem, the Go userbase doesn't want that. They want one way to do things and highly value consistency, idiomatic code, and not having to make unnecessary implementation choices when programming.

In every programming language, there is a cost to adding features, but that cost is relatively higher in Go.

2. Concurrency.

Concurrency, channels, and goroutines are central to the design of the language. While I'm sure you can combine exception handling with CSP-based concurrency, I wouldn't guarantee that the resulting language is easy to use correctly. What happens when an uncaught exception unwinds the entire stack of a goroutine? How does that affect other goroutines that it spawned or that spawned it? What does it do to goroutines that are waiting on channels that expect to hear from it?

There may be a good design there, but it may also be that it's just really really hard to reason about programs that heavily use CSP-style concurrency and exceptions for error handling.

The Go designers cared more about concurrency than error handling, so they chose a simpler error handling model that doesn't interfere with goroutines as much. (I understand that panics complicate this story. I'm not a Go expert. This is just what I've inferred from the outside.)

dcow · 8h ago
(2) hasn’t been a problem for Swift or Rust, both of which have the ability to spawn tasks willy nilly. I don’t think we’re talking about adding exceptions to Go, we’re talking about nicer error handling syntax.

(1) yes Go’s minimal language surface area means the thing you spend the most time doing in any program (handling error scenarios and testing correctness) is the most verbose unenjoyable braindead aspect. I’m glad there is a cultivated home for people that tolerate this. And I’m glad it’s not where I live…

hackingonempty · 13h ago
The language is designed for Google, which hires thousands of newly graduated devs every year. They also have millions of lines of code. In this environment they value easy of onboarding devs and maintaining the codebase over almost everything else. So they are saddled with bad decisions made a long time ago because they are extremely reluctant to introduce any new features and especially breaking changes.
philosophty · 12h ago
This is a common theme with criticisms of Go.

Relative amateurs assuming that the people who work on Go know less about programming languages than themselves, when in almost all cases they know infinitely more.

The amateur naively assumes that whichever language packs in the most features is the best, especially if it includes their personal favorites.

The way an amateur getting into knife making might look at a Japanese chef's knife and find it lacking. And think they could make an even better one with a 3D printed handle that includes finger grooves, a hidden compartment, a lighter, and a Bluetooth speaker.

Yoric · 8h ago
FWIW, I have designed several programming languages and I have contributed (small bits) to the design of two of the most popular programming languages around.

I understand many of Go's design choices, I find them intellectually pleasing, but I tend to dislike them in practice.

That being said, my complaints about Go's error-handling are not the `if err != nil`. It's verbose but readable. My complaints are:

1. Returning bogus values alongside errors.

2. Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled.

kiitos · 4h ago
> Returning bogus values alongside errors.

Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error.

> Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled

I don't see how any good-faith analysis of Go errors as specified/intended by the language and its docs, nor Go error handling as it generally exists in practice, could lead someone to this conclusion.

packetlost · 3h ago
> Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error

But you have to return something to satisfy the function signature's type, which often feels bad.

>> Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled

> I don't see how any good-faith analysis of Go errors as specified/intended by the language and its docs, nor Go error handling as it generally exists in practice, could lead someone to this conclusion.

I agree to a point, but if you look at any random Go codebase, they tend to use errors.New and fmt.Errorf which do not lend themselves to branching on error conditions. Go really wants you to define a type that you can cast or switch on, which is far better.

kiitos · 1h ago
> Go really wants you to define a type that you can cast or switch on, which is far better.

Go very very much does not want application code to be type-asserting the values they receive. `switch x.(type)` is an escape hatch, not a normal pattern! And for errors especially so!

> they tend to use errors.New and fmt.Errorf which do not lend themselves to branching on error conditions

You almost never need to branch on error conditions in the sense you mean here. 90% of the time, err != nil is enough. 9% of the time, errors.Is is all you need, which is totally satisfied by fmt.Errorf.

stouset · 17m ago
> 90% of the time, err != nil is enough

Only if your only desire is to bubble the error up and quite literally not handle it at all.

If you want to actually handle an error, knowing what actually went wrong is critical.

stouset · 20m ago
> Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error.

Ah yes the classic golang philosophy of “just avoid bugs by not making mistakes”.

Nothing stops you from literally just forgetting to handle ann error without running a bunch of third party linting tools. If you drop an error on the floor and only assign the return value, go does not care.

throwawaymaths · 10h ago
To be fair there are lots of people who have used multiple programming languages at expert levels that complain about go - in the same ways - as well! They might not be expert programming language designers, but they have breadth of experience, and even some of them have written their own programming languages too.

Assuming that all complainants are just idiots is purely misinformed and quite frankly a bit of gaslighting.

philosophty · 10h ago
"To be fair there are lots of pilots who have flown multiple aircraft at an expert level that complain about the Airbus A380 - in the same ways - as well! They might not be expert airplane designers, but they have a breadth of experience, and even some of them have created their own model airplanes too."

Yes, non-experts can have valid criticisms but more often than not they're too ignorant to even understand what trade-offs are involved.

throwawaymaths · 9h ago
see there you go again assuming. im talking about people who have written programming languages that are used in prod with millions of users, not people with toy languages.

is the entire go community this toxically ignorant?

philosophty · 9h ago
You said nothing that indicates you were referring to other expert language designers.

This entire thread is full if amateurs making ignorant comments. So what expert criticisms are you referring to?

You accused me of "gaslighting" and being "toxically ignorant" while I have been entirely civil.

mamcx · 6h ago
The problem is that error handling is far more complex than you think at first.

The idea that "the happy path is the most common" is a total lie.

    a + b
CAN fail. But HOW that is the question!

So, errors are everywhere. And you must commit to a way to handle it and no is not possible, like no, not possible to satisfy all the competing ideas about it.

So there is not viable to ask the community about it, because:

    a + b
CAN fail. But HOW change by different reasons. And there is not possible to have a single solution for it, precisely because the different reasons.

So, you pick a side and that is.

38 · 5h ago
that is weird that they call it a Wiki when it is not a wiki any more - you have to submit changes for approval
pie_flavor · 14h ago
You draw up a list of checkboxes, you debate each individual point until you can check them off, you don't uncheck them unless you have found a showstopping semantics error or soundness hole. Once it is complete it is implemented and then everyone who had opinions about whether it should be spelt `.await` or `/await` or `.await!()` vanishes back into the woodwork from whence they came. Where's the disconnect?

Rust works like this. Sometimes an issue can be delayed for over a decade, but eventually all the boxes are checked off and it gets stabilized in latest nightly. If Go cannot solve the single problem everyone immediately has with the language, despite multiple complete perfect proposals on how to do it, simply because they cannot pick between the proposals and are waiting for people to stop bikeshedding, then their process is a farce.

Thaxll · 13h ago
"despite multiple complete perfect proposals on how to do it"

There is no such a thing.

arccy · 13h ago
and this is how rust gains its reputation as an ugly to read language with inconsistent syntax: design by committee
codedokode · 12h ago
Golang is also ugly, for example some fields start with a capital letter and some do not.

Also I don't understand how to implement transparent proxies in Go for reactive UI programming.

joaohaas · 9h ago
If you don't care about field access just always write fields with uppercase. Any APIs you're using only expose uppercased variables as well, so it'll stay consistent.

The public/private stuff is mostly useful for publishing modules with sound APIs.

arccy · 10h ago
you don't Go is explicit about things.

maybe caps for export is ugly, it's not much different from how python hides with _

callc · 9h ago
I find both the golang uppercase lowercase naming scheme and python underscores for private (and __ for *extra* private) to be terrible design choices.

They are hidden functionality, a set of rules which must be remembered. “Make sure to do <weird trick> because that mean <X> in <PL>”

Leave identifier names alone. Packing extra info inside is unnecessary mental burden

mdaniel · 5h ago
Also, I recently learned that only leading __ are considered special, whereas both sets of __ go back to being a public method due to the dunder magic methods such as __add__, __eq__, etc
callc · 4h ago
Oh yeah, I didn’t even notice. Much worse than I thought!

Imagine explaining these rules to a beginner learning programming.

arccy · 8h ago
the other option seems worse, i dunno if i can use this identifier unless i have an IDE or i look up the definition every time
jeremyjh · 13h ago
Whereas Go has taken the approach of designing everything "later".
mseepgood · 13h ago
It's good to have languages with different approaches to design and and with different design philosophies.
olalonde · 9h ago
Isn't "design by popular vote" an extreme version of "design by committee"? Go won't implement error handling syntax because it can't reach community consensus.
arccy · 8h ago
Go may take community opinion into account, but just because something is popular doesn't mean the team will accept it. the language designers have the final say, and they will reject popular but bad ideas.
wtetzner · 13h ago
Ugly is subjective, but which part of the syntax is inconsistent?
j-krieger · 9h ago
I tended to disagree on this discussion in the past, but I increasingly no longer do. For example, let's have a look at the new `implicit lifetime capturing` syntax:

  fn f(x: &()) -> impl Sized + use<'_> { x }
It's weird. It's full of sigils. It's not what the Rust team envisioned before a few key members left.
joshuamorton · 8h ago
The only parts of this that I (as someone who has dabbled in rust, but barely interacted with explicit lifetimes, ever) am confused by is

"&()".

And I assume it is similar to some kind of implicit capture group in cpp ("[&]") and "`_", which is a lifetime of some kind. I don't know what the "use" keyword does, but it's not a sigil, and "->", "impl Sized", and "{"/"}" are all fairly self-explanatory.

I will say https://doc.rust-lang.org/edition-guide/rust-2024/rpit-lifet... does not answer any of my questions and only creates more.

Macha · 7h ago
It's a reference to a unit type. unit is pretty useless (like void in Java). It's sort of a silly thing you wouldn't do in real production code, just whoever wrote this example picked a type that is short to type, would be understood by a Rust programmer, and doesn't require any external context.
38 · 4h ago
"u8" would have done the job fine and actually been readable
dlisboa · 12h ago
I don't know if this qualifies as inconsistent, but:

`impl T for for<'a> fn(&'a u8) {}`

The `for` word here is used in two different meanings, both different from each other and from the third and more usual `for` loop.

Rust just has very weird syntax decisions. All understandable in isolation but when put altogether it does yield a hard to read language.

steveklabnik · 12h ago
All three have the same underlying idea: do this for every thing of that. In the first case, it's implement a trait for a type. In the second case, it's "for all choices of the lifetime" and for a for loop, it's do something for each element of a collection.
dlisboa · 12h ago
I understand how that seems logical in isolation but it's just not how syntax is usually read by people. It's done so as part of a reading context instead of as separate syntatical tokens. The underlying idea is not the same for the reader because the context is vastly different.
steveklabnik · 9h ago
Sure, and I think that's insightful: what you may consider a mess, I may consider orthogonal!
wtetzner · 11h ago
This feels disingenuous. I have a hard time imagining a case where someone would find this confusing.
NobodyNada · 12h ago
Rust tends to prefer reusing keywords (in unambiguously different contexts) rather than introducing new ones, because adding a new keyword is a backwards compatibility break that requires a new language edition and creates migration pain for anyone who had an identifier with the same name as the new keyword.
steveklabnik · 12h ago
While that's true, all three of these uses pre-date Rust 1.0, so there was total freedom in this case.
arccy · 13h ago
.await something that looks like a field access but actually does something else
pornel · 6h ago
Designs have trade-offs.

In this case, it was important for await and error handling with the ? operator to be readable together.

The order of operations in `await foo()?` is ambiguous, but `foo()?.await` and `foo().await?` have an obvious and clearly visible order. As a bonus, the syntax supports chaining multiple async operations without parenthesis. `fetch().await.body().await` is much nicer to write than `await (await fetch()).body()`.

Since `await` is a reserved keyword, `.await` can't be a field access. Editors with syntax highlighting can easily color it like a keyword.

The problem looking like a field has proven to be total a non-issue in practice. OTOH the syntax avoided major pitfall of visually unclear operator precedence, inconvenience of mixing of prefix and postfix operators, and ended up being clear and concise. It's been such a success, that users have since asked to have more keywords and macros usable in a postfix form.

dcow · 8h ago
This might be a valid point if Go wasn’t an atrociously ugly and verbose language with some of the worst design out there.
zaptheimpaler · 12h ago
This is the kind of criticism made by people who've spent less than a few days working with a language. Just glancing at some code from a distance. There's nothing actually wrong with it besides being foreign from what you are used to. After you gain some familiarity, it doesn't look ugly or beautiful it just looks like syntax.
kiitos · 11h ago
Programming languages are designed systems, they need to make sense holistically. They're not simply collections of tick-boxed features that are expected to be added once their tick-box requirements are satisfied.
827a · 7h ago
This opinion is clearly far more arguable than you might think.
hu3 · 14h ago
> Go cannot solve the single problem everyone immediately has with the language...

What? Survey says 13% mentioned error handling.

And some people actually do prefer it as is.

https://go.dev/blog/survey2024-h1-results

judofyr · 14h ago
13% mentioned that error handling was the biggest challenge with using Go. This was not a multiple choice question, but you had to pick one answer. We don't know how many people would consider it challenging. (This is typically why you have a 1-10 scale per choice.)
joaohaas · 9h ago
This doesn't mean the rest of the 87% enjoy it. Honestly, I'd rather the next survey included a question "are you satisfied with the current error handling approach"
827a · 7h ago
That survey specifically asked for the "biggest" challenge. One could make a compelling argument for the survey answer "learning how to write Go effectively" being an extremely bad option to put on a survey, because it at-least partially catch-alls every other answer. Its no wonder it got first place.
TZubiri · 3h ago
!RemindMe in 25 years.

Did Rust become a clusterfuck like C++?

Is Go as timeless as it was during release?

GoatInGrey · 13h ago
> complete perfect

This is entirely subjective and paints the Go community as being paradoxical, simultaneously obstinate and wanting change.

The disappointing reality is that Go's error handling is the least terrible option in satisfying the language design ethos and developers writing Go. I have a penchant for implementing V's style of error handling, though I understand why actually implementing it wouldn't be all sunshine and rainbows.

pie_flavor · 13h ago
No, actually, an operator that's essentially a macro for this entirely boilerplate operation would be less terrible, exactly the same decision Rust made for the exact same reason. So would Cox's proposal, so would others. Doing nothing, as a permanent solution, because you can't figure out which of the better things you should do, is not a virtue. You may be confusing it with the virtue of holding out on simpler solutions while a better solution is implemented, or the virtue of not solving things which aren't problems, but this is a problem and they have intentionally blocked the solution indefinitely.
tialaramex · 13h ago
Rust's try! macro was† "essentially a macro for this entirely boilerplate operation" but the Try operator ? is something more interesting because in the process they reified ControlFlow.

Because implementing Try for your own custom types is unstable today if you want to participate you'd most likely provide a ControlFlow yourself. But in doing that you're making plain the distinction between success/ failure and early termination/ continuing.

† Technically still is, Rust's standard library macros are subject to the same policies as the rest of the stdlib and so try! is marked deprecated but won't be removed.

kiitos · 11h ago
It's just simply not the cause that error handling is an "entirely boilerplate operation", nor that any kind of macro in Go "would be less terrible" than the status quo, nor is it true that decisions that Rust made are even applicable to Go. Believe it or not, the current approach to error handling actually does work and actually is better than most/all proposals thru the lens of Go's original design intent.
pie_flavor · 6h ago
This code:

    foo, err := someExpr
    if err != nil {
        return nil, err
    }
Is entirely boilerplate, and a language feature could generate it (and in Rust, does). This is not the same statement as 'all error handling is boilerplate', which is obviously false, which is why I didn't say that. Condensing that particular snippet down to `?` would be less terrible than the status quo, where the status quo is every function being filled with twenty copies of it drastically reducing readability. The situation is exactly the same as with old Rust, where:

    let foo = match expr {
        Ok(val) => val,
        Err(e) => return e,
    };
Was entirely boilerplate. Rust noticed that this was a problem and solved it. Go's status quo is not better than pre-`?` Rust's status quo; it does nothing pre-`?` Rust didn't. Go just doesn't solve it.

It is not actually the original design intent of Go to make every function 50% boilerplate garbage by LoC. Go is extremely full of 'helpful' happy-path short functions that leave you reimplementing lots of stuff more verbosely the moment you step off the happy path, inclusive of happy paths that do partially the wrong thing. `?` is exactly in line with `iota`, `foo_windows.go`, `flag.Var`, `http.HandleFunc`, etc. I don't know why people respond to literally every Go mistake with 'it's actually not a mistake, you just don't understand the genius', especially since half the mistakes are reverted later and acknowledged as mistakes.

kiitos · 5h ago

    This code:
    
        foo, err := someExpr
        if err != nil {
            return nil, err
        }

    Is entirely boilerplate
But you'd never write that, you'd write

    if err != nil {
        return nil, fmt.Errorf("some expr: %w", err)
    }
which is _not_ boilerplate, in any sense that would benefit from being mitigated with new syntax or short-cuts.

> Condensing that particular snippet down to `?` would be less terrible than the status quo

This simply isn't any kind of objective or agreed-upon truth. Many people, including myself, believe that the status quo is better than what you're suggesting here.

People who are annoyed with Go at some fundamental level, and who largely don't use the language themselves, delight in characterizing `if` blocks related to errors as "boilerplate" that serves no purpose, and needs to be addressed at a language level.

> `?` is exactly in line with `iota`, `foo_windows.go`, `flag.Var`, `http.HandleFunc`, etc.

I've thought on this at length and I have no clue as to what you think the common property between these things might be. A proposed language sigil that impacts control-flow, an existing keyword that's generally not recommended for use, a build-time filename convention, and two unrelated stdlib type definitions?

pie_flavor · 5h ago
You say 'you would never', 'generally not recommended', etc., about things that dominate all code in the wild. Perhaps you do not understand the Go vision. Yes, in both Go and Rust, people should add context to errors; and in both Go and Rust, they don't. Cox's proposal provides something slightly smarter than raw `?`, while Rust was designed smarter from the start and it just takes a library to do that (`snafu` or `anyhow`).

> I've thought on this at length and I have no clue as to what you think the common property between these things might be.

They are examples of the common property I specifically stated in the preceding sentence:

> Go is extremely full of 'helpful' happy-path short functions that leave you reimplementing lots of stuff more verbosely the moment you step off the happy path, inclusive of happy paths that do partially the wrong thing.

(In point of fact you shouldn't use fmt.Errorf if you're serious about errors either; it cannot be usefully inspected at runtime. You want an explicitly declared error type for that.)

kiitos · 5h ago
> Rust was designed smarter from the start

I guess this makes it pretty clear that there's no useful conversation to be had with you on this topic.

> (In point of fact you shouldn't use fmt.Errorf if you're serious about errors either; it cannot be usefully inspected at runtime. You want an explicitly declared error type for that.)

You don't need a discrete error type to allow callers to inspect returned errors at runtime -- `fmt.Errorf("annotation: %w", err)` allows callers to check for sentinel errors via `errors.Is` -- which is the overwhelmingly most common case.

bravesoul2 · 11h ago
If Haskell was mainstream and everyone piled in and complained that objects were immutable and it adds so much noise having to deal with that using lenses or state monads or whatever, do we go with democracy or do we say wait.... maybe Haskell was meant to be like this, there are reasons something is a seperate language.
_jab · 14h ago
I once had a Go function that, unusually, was _expecting_ an error to be returned from an inner function, and so had to return an error (and do some other processing) if none was returned by the inner function, and return nil if the inner function did return an error.

In a nutshell, this meant I had to do `if err == nil { // return an error }` instead of `if err != nil { ... }`. It sounds simple when I break it down like this, but I accidentally wrote the latter instead of the former, and was apparently so desensitized to the latter construct that it actually took me ages to debug, because my brain simply did not consider that `if err != nil` was not supposed to be there.

I view this as an argument in favor of syntactic sugar for common expressions. Creating more distinction between `if err != nil` (extremely common) and `if err == nil` (quite uncommon) would have been a tangible benefit to me in this case.

adamrt · 14h ago
Any time I write "if err == nil" I write // inverted just to make it stick out. It would be nice if it was handled by the language but just wanted to share a way to at least make it a bit more visible.

    if err == nil { // inverted
        return err
    }
macintux · 13h ago
I know diddly/squat about Go, but from similar patterns in aeons past, would "nil == err" work as a way to make it stand out?
haiku2077 · 11h ago
Just tried this and it appears to be valid in the compiler, formatter and golangci-lint
_whiteCaps_ · 10h ago
https://en.wikipedia.org/wiki/Yoda_conditions

Works especially well in languages that can make assignments in if statements, e.g:

if foo = 42 { }

macintux · 8h ago
Thank you, I was unaware of this label. Quite descriptive.
hnlmorg · 13h ago
I do something similar. I leave a comment but with a short comment why it’s inverted.

It’s usually pretty obvious why: eg

    if err == nil { 
         // we can exit early because we don’t need to keep retrying
But it at least saves me having to double check the logic of the code each time I reread the code for the first time in a while.
vhcr · 12h ago
Would be nice if code editors colored it differently so it's easier to see.
prerok · 11h ago
return nil

would be clearer, I think. Seems like it's the same but would color differently in my editor.

9rx · 13h ago
Of course, `if fruit != "Apple" { ... }` would leave you in the exact same situation. Is there a general solution to improving upon this? Seeing it as an error problem exclusively seems rather misguided. After all, there is nothing special or unique about errors. They're just state like any other.
adamrt · 13h ago
I think its more of a comment that "err != nil" is used in the vast majority of cases, so you start to treat it as noise and skim it.
9rx · 13h ago
That reality may make the fundamental flaws of the if statement more noticeable, but at the end of the day the problem is still that the if statement itself is not great. If we're going to put in effort to improve upon it – and it is fair to say that we should – why only for a type named error?
saghm · 4h ago
Because the type named error is used in that flawed way orders of magnitude more than any other type. If there were other types that were consistently used as the last return value in functions that short-cirucuited when calling other functions that retuned specific sentinels in their final value when called, there would be reason to do it for them too.

In fact, this is exactly what Rust's ? -operator already does, and something that's obscured by the oddness of using pseudo-tuples to return errors alongside non-error values rather than requiring exactly one or the other; `Result` in Rust can abstract over any two types (even the same one for success and error, if needed), and using the ?-operator will return the value from the containing function if it's wrapped by `Err` or yield it in the expression if it's wrapped by `Ok`. In Go, the equivalent would be to have the operator work on `(T, E)` where `T` and `E` could be any type, with `E` often but not always being an error. Of course, this runs into the issue of how to deal with more than two return values, but manually wrap the non-error values into a single type in order to use the operator would solve that with overall way less boilerplate than what's required currently due to it being rarely needed.

9rx · 2h ago
> Because the type named error is used in that flawed way orders of magnitude more than any other type.

That does not give reason to only solve for a narrow case when you can just as well solve for all cases.

> If there were other types that were consistently used as the last return value in functions that short-cirucuited when calling other functions that retuned specific sentinels in their final value when called, there would be reason to do it for them too.

Which is certainly the situation here. (T, bool) is seen as often as (T, error) – where bool is an error state that indicates presence of absence of something. Now that your solution needs to cover "error" and "bool", why not go all the way and include other types too?

Errors are not limited to "error" types. Every value, no matter the type, is potentially an error state. bool is an obvious case, but even things like strings and integers can be errors, depending on business needs. So even if you truly only want to solve for error cases, you still need to be able to accommodate types of every kind.

The computer has no concept of error. It is entirely a human construct, so when handling errors one has to think about from the human perspective or there is no point, and humans decidedly do not neatly place errors in a tightly sealed error box.

> rather than requiring exactly one or the other

That doesn't really make sense in the context of Go. For better or worse, Go is a zero value language, meaning that values always contain useful state. It is necessarily "choose one or the other or both, depending on what fits your situation". "Result" or other monadic-type solutions make sense in other languages with entirely different design ideas, but to try and graft that onto Go requires designing an entirely new language with a completely different notion about how state should be represented. And at that point, what's the point? Just use Rust – or whatever language already thinks about state the way you need.

> but manually wrap the non-error values into a single type in order to use the operator would solve that

I'm not sure that is the case. Even if we were to redesign Go to eliminate zero values to make (T XOR E) sensible, ((T AND U) XOR E) is often not what you want in cases where three or more return arguments are found. (T, bool, error) is a fairly common pattern too, where both bool and error are error states, similar to what was described above. ((T AND U) XOR E) would not fit that case at all. It is more like ((T XOR U) OR (T XOR E)).

I mean, realistically, if we completely reimagined Go to be a brand new language like you imagine then it is apparent that the code written in it would look very different. Architecture is a product of the ecosystem. It is not a foregone conclusion that third return arguments would show up in the first place. But, for the sake of discussion...

purpleidea · 9h ago
This is actually an argument against the syntactic changes. Because now if you have the common `if err == nil { return ... }` pattern, then you have _that_ "littering" your code, instead of the syntax.

The current solution is fine, and it seems to be only junior/new to golang people who hate it.

Everyone I know loves the explicit, clear, easy to read "verbose" error handling.

scubbo · 8h ago
> then you have _that_ "littering" your code, instead of the syntax.

Yes, exactly. The unusual thing _should_ look unusual.

derefr · 11h ago
Just as a devil's-advocate argument, an IDE + font could syntax-highlight + ligature `if err != nil` (only under Golang syntax mode) into a single compact heiroglyph and fade it into the background — which would in turn make anything that differs from that exact string (like `if err == nil`) now pop out, due to not being rendered that way.
saghm · 4h ago
The same logic could apply to the oppositions they cited to the `try` function though; an editor could easily make it stick out to alleviate it blending in when nested inside blocks. This is exactly why nobody ever accidentally confuses `.await` in Rust for a struct field even though from a plaintext perspective it's syntactically identical. If you're going to utilize the editor to carry the heavy weight, you might as well just pick literally any new syntax that replaces all of the extra typing with something more terse.
skybrian · 14h ago
Good point. Perhaps it could also be solved in an editor with a collapsed notation like ‘if err … {‘
matthewmueller · 6h ago
I like nil == err for this case
TZubiri · 3h ago
Nothingburger here, you had a bug, and you fixed it.

All is well, no need to question your language or the meaning of life.

When you make a mistake irl or trip over when walking, do you reconsider you DNA and submit a patch to God?

Sometimes you just gotta have faith in the language and assume it like an axiom, to avoid wasting energy fighting windmills.

I'm not a deep Go programmer, but I really enjoy how it's highly resistant to change and consistent across it's 15 years so far.

jamamp · 13h ago
I like Go's explicit error handling. In my mind, a function can always succeed (no error), or either succeed or fail. A function that always succeeds is straightforward. If a function fails, then you need to handle its failure, because the outer layer of code can not proceed with failures.

This is where languages diverge. Many languages use exceptions to throw the error until someone explicitly catches it and you have a stack trace of sorts. This might tell you where the error was thrown but doesn't provide a lot of helpful insight all of the time. In Go, I like how I can have some options that I always must choose from when writing code:

1. Ignore the error and proceed onward (`foo, _ := doSomething()`)

2. Handle the error by ending early, but provide no meaningful information (`return nil, err`)

3. Handle the error by returning early with helpful context (return a general wrapped error)

4. Handle the error by interpreting the error we received and branching differently on it. Perhaps our database couldn't find a row to alter, so our service layer must return a not found error which gets reflected in our API as a 404. Perhaps our idempotent deletion function encountered a not found error, and interprets that as a success.

In Go 2, or another language, I think the only changes I'd like to see are a `Result<Value, Failure>` type as opposed to nillable tuples (a la Rust/Swift), along with better-typed and enumerated error types as opposed to always using `error` directly to help with error type discoverability and enumeration.

This would fit well for Go 2 (or a new language) because adding Result types on top of Go 1's entrenched idiomatic tuple returns adds multiple ways to do the same thing, which creates confusion and division on Go 1 code.

barrkel · 13h ago
My experience with errors is that error handling policy should be delegated to the caller. Low level parts of the stack shouldn't be handling errors; they generally don't know what to do.

A policy of handling errors usually ends up turning into a policy of wrapping errors and returning them up the stack instead. A lot of busywork.

XorNot · 10h ago
At this point I make all my functions return error even if they don't need it. You're usually one change away from discovering they actually do.
billmcneale · 11h ago
> If a function fails, then you need to handle its failure

And this is exactly where Go fails, because it allows you to completely ignore the error, which will lead to a crash.

I'm a bit baffled that you correctly identified that this is a requirement to produce robust software and yet, you like Go's error handling approach...

haiku2077 · 11h ago
On every project I ship I require golangci-lint to pass to allow merge, which forces you to explicitly handle or ignore errors. It forbids implicitly ignoring errors.

Note that ignoring errors doesn't necessarily lead ti a crash; there are plenty of functions where an error won't ever happen in practice, either because preconditions are checked by the program before the function call or because the function's implementation has changed and the error return is vestigal.

etra0 · 11h ago
Yet the problem still has happened on big projects:

https://news.ycombinator.com/item?id=36398874

pphysch · 10h ago
> which will lead to a crash

No it won't. It could lead to a crash or some other nasty bug, but this is absolutely not a fact you can design around, because it's not always true.

stock_toaster · 7h ago
I just want borgo[1] syntax to be the Go 2 language. A man can dream...

[1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo

LinXitoW · 8h ago
I have to ask, in comparison to what do you like it? Because every functional language, many modern languages like Rust, and even Java with checked exceptions offers this.

Hell, you can mostly replicate Gos "error handling" in any language with generics and probably end up with nicer code.

If your answer is "JavaScript" or "Python", well, that's the common pattern.

ignoramous · 9h ago
At this rate, I suspect Go2 is an ideas lab for what's never shipping.
maxwellg · 15h ago
This is the right move for Go. I have grown to really love Go error handling. I of course hated it when I was first introduced to the language - two things that changed that:

- Reading the https://go.dev/blog/errors-are-values blog post (mentioned in the article too!) and really internalizing it. Wrote a moderately popular package around it - https://github.com/stytchauth/sqx

- Becoming OK with sprinkling a little `panic(err)` here and there for truely egregious invalid states. No reason forcing all the parent code to handle nonsense it has no sense in handling, and a well-placed panic or two can remove hundreds of error checks from a codebase. Think - is there a default logger in the ctx?

masklinn · 14h ago
This is just sad. Neither of your supports have anything to do with how dismal Go's error handling is, and neither would be worsened in any way by making it better. If anything they would be improved.
bravesoul2 · 11h ago
Me too. I'll take the higher Loc for the greater certainty of what is going on.

I thought it was clever in C# years ago when I first used to to grok all the try/catch/finally flows including using and nested versions and what happens if an error happens in the catch and what if it happens in the finally and so on. But now I'd rather just not think about that stuff.

pas · 12h ago
Even PHP has better error handling with the levels, and the @ (at operator) to suppress errors at a callsite.

Even bash has -e :)

evertedsphere · 8h ago
rust-style "sum type" errors are values too
Mond_ · 13h ago
I really don't like how this article claims that the primary issue with Go's error handling is that the syntax is too verbose. I don't really care about that.

How about:

- Errors can be dropped silently or accidentally ignored

- function call results cannot be stored or passed around easily due to not being values

- errors.Is being necessary and the whole thing with 'nested' errors being a strange runtime thing that interacts poorly with the type system

- switching on errors being hard

- usage of sentinel values in the standard library

- poor interactions with generics making packages such as errgroup necessary

Did I miss anything?

closeparen · 8h ago
90% of working professionally in Go is contriving test cases to achieve statement coverage over each error return branch, something no one would ever do in a language with exceptions.
cedws · 7h ago
Test coverage as a target metric is stupid.
g7r · 2h ago
Maybe so. However, it is really really handy when you are assessing the completeness of your tests using code coverage, and can clearly see unhandled negative paths. And then you can decide whether some of them deserve dedicated tests or not.
MeetingsBrowser · 2h ago
why?
neild · 12h ago
> I really don't like how this article claims that the primary issue with Go's error handling is that the syntax is too verbose.

I don't believe this claim is made anywhere.

We've decided that we are not going to make any further attempts to change the syntax of error handling in the foreseeable future. That frees up attention to consider other issues (with errors or otherwise).

pas · 12h ago
writing a small book on the topic and somehow missing it is the point, not that technically the text is claiming it, the subtext is doing it.
GiorgioG · 13h ago
Just remember it took FOREVER for Go to support some form of generics. Go evolution happens at a glacial pace. That's a feature, not a bug....to many.
VirusNewbie · 12h ago
Agreed, 100%.

We're both Googlers here and this is so disappointing to be let down again by the Go team.

tsimionescu · 14h ago
> Going back to actual error handling code, verbosity fades into the background if errors are actually handled. Good error handling often requires additional information added to an error. For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error. In this (admittedly contrived) example, the relative amount of boilerplate is much smaller:

  [...] 
  if err != nil {
        return fmt.Errorf("invalid integer: %q", a)
    }
  [...] 
It's so funny to me to call "manually supplying stack traces" as "handling an error". By the Go team's definition of handling errors, exceptions* "automatically handle errors for you".

* in any language except C++, of course

teeray · 14h ago
It's funny to me when people see screenfuls of stack traces and remark how clear and useful it is. Perhaps, but do you really need all that? At what cost to your logs? I'd much rather have a one-liner wrapped error that cuts through all the framework and runtime noise. Yes, I can trace just as effectively (usually better)--the wrapping is very greppable when done well. No, in over a decade of writing Go full time, I have never cared about a runtime function or the other usual verbose garbage in my call stack.
rwiggins · 9h ago
> how clear and useful it is. Perhaps, but do you really need all that?

Do I need clear and useful things? Maybe not. Would I like to have them anyway? Yes.

Years ago, I configured a Java project's logging framework to automatically exclude all the "uninteresting" frames in stack traces. It was beautiful. Every stack trace showed just the path taken through our application. And we could see the stack of "caused-by" exceptions, and common frames (across exceptions) were automatically cut out, too.

Granted, I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features...

And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.

> the wrapping is very greppable when done well

Yeah, that's my other problem with it. _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`). (Or use `%v` if I don't want to wrap. Yet another little nuance I find myself needing to explain to junior devs over and over. And don't get me started about putting that in the `fmt` package.)

So anyway, I've seen monstrosities of errors where there were 6+ "statement: statement: statement: statement: statement: final error" that felt like a dark comedy. I've also seen very high-level errors where I dearly wished for some intermediate context, but instead just had "failed to do a thing: EOF".

That all being said, stack traces are really expensive. So, you end up with some "fun" optimizations: https://stackoverflow.com/questions/58696093/when-does-jvm-s...

teeray · 7h ago
> _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`)

Easy. Always wrap. Wrap with what you were doing when the error occurred.

> I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features... And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.

Yeah, despite the proliferation of IDEs for Go in recent years, Go has traditionally been pretty anti- big-iron IDE.

jitl · 7h ago
Whenever I get an error in NodeJS without a stack trace I am pretty pissed off about it. When my program is failing for some reason I really want to know where it came from, and the stack is hugely helpful in narrowing down the possibility space.
d3ckard · 13h ago
From the Elixir's developer perspective, this is insane. The issue is solved in Erlang / Elixir by functions commonly returning {:ok, result} or {:error, description_or_struct} tuples. This, together with Elixir's `with` statement allows to group error handling at the bottom, which makes for much nicer readability.

Go could just add an equivalent of `with` clause, which would basically continue with functions as long as error is nil and have an error handling clause at the bottom.

tyre · 8h ago
From all available evidence, there is no chance in hell Go could adopt a `with` statement.

Go is fascinating in how long it holds out on some of the most basic, obviously valuable constructs (generics, error handling, package management) because The Community cannot agree.

- Generics took 13 years from the open source release.

- 16 years in there isn’t error handling.

- Package management took about 9 years.

There’s value to deliberation and there’s value to shipping. My guess is that the people writing 900 GH comments would still write Go and be better off by the language having something vs. kicking the can down the road.

MeetingsBrowser · 2h ago
> My guess is that the people writing 900 GH comments would still write Go and be better off by the language having something vs. kicking the can down the road.

My guess is they will still write Go even if error handling stays the same forever.

throwawa14223 · 13h ago
Go's multiple return is in itself insane from my perspective. You cannot 'do' anything with a function that has multiple return types except assign them to a variable.
masklinn · 12h ago
The saddest part is that Go's designers decided to use MRV but pretty much just as bad tuples: as far as I can tell the only thing Go uses MRV for which tuple wouldn't be better as is post-updating named return values, and you could probably just update those via the container.

Common Lisp actually does cool things (if a bit niche) things with MRVs, they're side-channels through which you can obtain additional information if you need it e.g. every common lisp rounding functions returns rounded value... and the remainder as an extra value.

So if you call

  (let ((v (round 5 2)))
    (format t "~D" v))
you get 2, but if you

  (multiple-value-bind (q r) (round 5 2)
    (format t "~D ~D" q r))
you get 2 and 1.
bravesoul2 · 11h ago
You can at least in Go do this:

r, err := f()

r := f()

_, err := f()

masklinn · 3h ago
No, you can not do the second one.

The other two are completely routine and will work just fine with lists in JS or tuples in Python or Rust (barring a few syntactic alterations).

knutzui · 12h ago
That's technically not true.

You can pass multiple return values of a function as parameters to another function if they fit the signature.

for example:

  func process[T any](value T, err error) {
    if err != nil {
      // handle error
    }
    // handle value
  }

this can be used in cases such as control loops, to centralize error handling for multiple separate functions, instead of writing out the error handling separately for each function.

  for {
    process(fetchFoo(ctx))
    process(fetchBar(ctx))
  }
prerok · 11h ago
Well, if fetchBar requires fetchFoo to complete successfully, you still somehow have to handle it.

That said, there are libraries out there that implement Result as generic type and it's fine working with them, as well.

I don't see what the hubbub is all about.

9rx · 12h ago
What else would you want to do with them? Maybe in rare cases you'd want to structure them into an array or something, but the inverse isn't possible either [e.g. func f(a, b, c int) -> f(<destructure array into arguments>)] so it isn't like it is inconsistent.

Perhaps what you are really trying to say is that multiple function arguments is insane full stop. You can pass in an array/tuple to the single input just the same. But pretty much every language has settled on them these days – so it would be utterly bizarre to not support them both in and out. We may not have known any better in the C days, but multiple input arguments with only one output argument is plain crazy in a modern language. You can't even write an identity function.

throwawaymaths · 11h ago
have a single return value and if you really need MRV, return as a tuple type, which you could destructure.

(this is what zig does)

9rx · 11h ago
But then why accept multiple input arguments? Why not limit to a single argument, accepting a tuple where multiple arguments are necessary, for input too?

Where multiple input arguments are present, not having multiple output arguments is just strange.

throwawaymaths · 10h ago
Exactly. Why not make multiple argument function call syntatic sugar over a ~single argument tuple call?

https://ziglang.org/documentation/0.14.1/#call

9rx · 7h ago
What would you need syntax sugar for? If you are going to support multiple arguments you may as well do it properly or don't do it at all. A poor bandaid because you fucked up tuples is not a quality that is to strive for.
juped · 1h ago
Haskellers and Rust fans think they own sum types, and people read their comments and blog posts and believe them, and decide they don't want sum types because they don't want to go down the horrifying Hindley-Milner rabbit hole.

But meanwhile it's just perfectly idiomatic Erlang and Elixir, none of that baggage required. (In fact, the sum types are vastly more powerful than in the ML lineage - they're open.)

JamesSwift · 14h ago
I havent followed this argument closely so forgive me if I'm missing relevant discussion, but I dont see why the Rust style isnt just adopted. Its the thing I immediately add now that I have generics in Go.

I only see this blurb in a linked article:

> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.

But I fail to see how having convenience equates to ignoring the error. Thats basically half of my problem with Go's approach, that nothing enforces anything about the result and only minimally enforces checking the error. eg this results in 'declared and not used: err'

  x, err := strconv.Atoi("123")
  fmt.Println("result:", x)
but this runs just fine (and you will have no idea because of the default 0 value for `y`):

  x, err := strconv.Atoi("123")
  if err != nil {
    panic(err)
  }
  y, err := strconv.Atoi("1234")
  fmt.Println("result:", x, y)
this also compiles and runs just fine but again you would have no idea something was wrong

  x, err := strconv.Atoi("123")
  if err != nil {
  }
  fmt.Println("result:", x)
Making the return be `result` _enforces_ that you have to make a decision. Who cares if someone yolos a `!` or conveniently uses `?` but doesnt handle the error case. Are you going to forbid `panic` too?
umanwizard · 14h ago
Go can’t have Result because they don’t have sum types, and they can’t add them because of their bizarre insistence that every type has to have a designated zero value.
stouset · 12m ago
> every type has to have a designated zero value

This bonkers design decision is, as far as I can tell, the underlying infectious cause of nearly every real issue with the language.

masklinn · 14h ago
> they can’t add them because of their bizarre insistence that every type has to have a designated zero value.

Nothing prevents adding union types with a zero value. Sure it sucks, but so do universal zero values in pretty much every other situation so that's not really a change.

umanwizard · 13h ago
Making it so all sum types have to be nillable would make them dramatically worse (the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL). I guess this is in agreement with your point.
tubthumper8 · 3h ago
I've read elsewhere that one idea for the zero value of sum types is not making it nillable, but rather making the zero value as the 0th variant of the sum type (and if it has associated data, the zero value of that as well).

It's weird, but does align with design decisions that have already been made.

So if there was an `Option[T]` with variants `None` and `Some[T]`, the zero value would be `None` because that's the zero-th variant

ndriscoll · 9h ago
In Scala basically all types are nullable (including Option/Try/Either), but it never really comes up because no one uses nulls (except when wrapping java code) because the language makes it easier to just do the right thing almost all of the time.

In fact the lack of sum types seems to be why you need everything to have a zero value in the first place: because sums are products you need a nonsensical value when the sum is the other type.

masklinn · 11h ago
> the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL

I don't think that's the case in Go: whereas I got the impression the C# team started souring on default() after generics landed (possibly because nullable value types landed alongside and they found out that worked just fine and there was no reason nullable reference types wouldn't) I don't really get that impression from the Go team, even less so from them still mostly being Googlers (proto3 removed both required fields and explicit default values).

masklinn · 14h ago
> But I fail to see how having convenience equates to ignoring the error.

The convenience of writing `?` means nobody will bother wrapping errors anymore. Is what I understand of this extremely dubious argument.

Since you could just design your `?` to encourage wrapping instead.

NobodyNada · 14h ago
> Since you could just design your `?` to encourage wrapping instead.

Which is exactly what Rust does -- if the error returned by the function does not match the error type of `?` expression, but the error can be converted using the `From` trait, then the conversion is automatically performed. You can write out the conversion implementation manually, or derive it with a crate like thiserror:

    #[derive(Error)]
    enum MyError {
        #[error("Failed to read file")
        IoError(#[from] std::io::Error)
        // ...
    }

    fn foo() -> Result<(), MyError> {
        let data = std::fs::read("/some/file")?;
        // ...
    }
You can also use helper methods on Result (like `map_err`) for inserting explicit conversions between error types:

    fn foo() -> Result<(), MyError> {
        let data = std::fs::read("/some/file").map_err(MyError::IoError)?;
        // ...
    }
masklinn · 13h ago
1. That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping.

2. Idiomatic go type erases errors, so you're converting from `error` to `error`, hence type-directed conversions are not even remotely an option.

NobodyNada · 12h ago
`map_err` does not need to be type-directed; you can use an arbitrary function or closure. An enum variant can be used as a function mapping from the variant type to the error type, but we can do any arbitrary transformation:

    .map_err(|e| format!("Failed to read file: {e}")?;
But the "idiomatic Go" way of doing things sounds a lot closer to anyhow in Rust, which provides convenience utilities for dealing with type-erased errors:

    use anyhow::{Result, Context};

    fn foo() -> Result<()> {
        let data = std::fs::read("/some/file").context("Failed to read file")?;
        // ...
    }
masklinn · 12h ago
Yes, I know that, but the argument (which, again, I called dubious) is that in both cases it's much easier to do just e.g.

  fn foo() -> Result<()> {
      let data = std::fs::read("/some/file")?;
      // ...
  }
whereas the current morass of Go's error handling means adding wrapping is not much more of a hassle.

But of course even if you accept that assertion you can just design your version of `?` such that wrapping is easier / not wrapping is harder (as it's still something you want) e.g. make it `?"value"` and `?nil` instead of `?`, or something.

alexchamberlain · 13h ago
> That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping.

In practice, the error type will be defined quite close to where the conversion is applied, so the static nature of it doesn’t feel too big.

tayo42 · 9h ago
You need to implement from for every type of error then? That seems pretty tedious also.
sa46 · 13h ago
> The convenience of writing `?` means nobody will bother wrapping errors anymore.

A thread from two days ago bemoans this point:

https://news.ycombinator.com/item?id=44149809

mseepgood · 13h ago
- It has poor visibility, it hides control flow branches in a single statement / expression. That's one of the reasons Go got rid of the ternary operator in favor of an if statement where each branch has to be on its own line.

- It isn't easily breakpointable.

- It favors "bubbling up" as-is over enriching or handling.

9rx · 14h ago
> I dont see why the Rust style isnt just adopted.

Mostly because it is not entirely clear what the Rust-style equivalent in Go might be. What would Rust's "From" look like, for example?

masklinn · 14h ago
> What would Rust's "From" look like, for example?

Idiomatic Go type-erases error types into `error`, when there is even a known type in the first place.

Thus `From` is not a consideration, because the only `From` you need is

    impl<'a, E> From<E> for Box<dyn Error + 'a>
    where
        E: Error + 'a,
and that means you can just build that in and nothing else (and it's really already built-in by the implicit upcasting of values into interfaces).
JamesSwift · 14h ago
Sorry, I wasnt specific in that part. When I say 'rust style' Im really just referring to a union type of `result | error`, with a way to check the state (eg isError and isResult) along with a way to get the state (eg getResult and getError). Optionally '?' and '!' as sugar.

That said, the other responder points out why the sum type approach is not favored (which is news to me, since like I said I havent followed the discussion)

kbolino · 14h ago
It's an interesting idea. Right now, you can do something like this:

    res := someFunc() // func() any
    switch v := res.(type) {
    case error:
        // handle error
    case T:
        // handle result
    default:
        panic("unexpected type!")
    }
Then, presumably, a T|error sum type would be a specialization of the any type that would allow you to safely eliminate the default arm of the switch statement (or so I would like to think -- but the zero value issue rears its ugly head here too). Personally, I'd also like to see a refinement of type switches, to allow different variable names for each arm, resulting in something like the following hypothetical syntax:

    switch someFunc().(type) {
    case err := error:
        // handle error
    case res := T:
        // handle result
    }
However, there's no real syntactic benefit for error handling to be found here. I like it (I want discriminated unions too), but it's really tangential to the problem. I'd honestly prefer it more for other purposes than errors.
hobs · 14h ago
To be fair Rust doesn't have sum type it has enums, which I feel like you could do in Go, but I haven't read the arguments.
hmry · 13h ago
Enum is what they call it (perhaps to appear more familiar to C++ programmers?), but from a computer science standpoint they are classic sum types.
9rx · 14h ago
Technically, Rust has sum types (a.k.a. tagged unions) that use an enum to generate the tag. So, while enumeration is involved, sum types is still a better description of what it is.
9rx · 14h ago
In Go, values are to be always useful, so `result | error` would be logically incorrect. `(result, result | error)`, perhaps – assuming Go had sum types, but that's a bit strange to pass the result twice.

Just more of the pitfalls of it not being clear how Rust-style applies to an entirely different language with an entirely different view of the world.

JamesSwift · 14h ago
What is useful about the value of `x` in the following code?

  x, err := strconv.Atoi("this is invalid")
On the contrary, `x` is _lying_ to you about being useful and you have absolutely no idea if the string was "0" or "not zero"
9rx · 14h ago
> and you have absolutely no idea if the string was "0" or "not zero"

You do – err will tell you. But in practice, how often do you really care?

As Go prescribes "Make the zero value useful" your code will be written in such a way that "0" is what you'll end up using downstream anyway, so most of the time it makes no difference. When it does, err is there to use.

That might not make sense in other languages, but you must remember that they are other languages that see the world differently. Languages are about more than syntax – they encompass a whole way of thinking about programs.

kbolino · 13h ago
I think it's worth noting that, while the general consensus has converged around (T, error) meaning T XOR error, it does not necessarily mean that. There are some places that violate this assumption, like the io.Reader and io.Writer interfaces. Especially io.Reader, where you can have (n>0, io.EOF), which also isn't even a proper error condition! (This isn't a big problem, though, since you rarely need to directly call Read or Write).

No comments yet

kiitos · 11h ago
If a function `func foo() (int, error)` returns a non-nil error, then the corresponding `int` is absolutely invalid and should never be evaluated by the caller, unless docs explicitly say otherwise.
XorNot · 10h ago
And this is why I prefer exceptions.

Errors are common but they are errors: they absolutely represent an exceptional branch of your control flow every time.

It seems reasonable to ask if that int should even be available in the control flow syntactically.

kiitos · 8h ago
Errors are just values, same as other values, it's in no way "exceptional" for a caller to get an error back from a call to some other code. If a function can fail it needs to return an error, if a function call fails the caller needs to deal with that error, not difficult stuff here. "Happy path" is no more or less important than "sad path" and both should be equally represented in the source code as written.
XorNot · 7h ago
That's doctrine. Saying it doesn't make it useful.

A program serves a business need: so it's well recognized that there's a distinction between business logic, and then implementation details.

So there's obviously no such thing as "just an error" from that alone: because "a thing failed because we ran out of disk space" is very different to "X is not valid because pre-1984 dated titles are not covered under post-2005 entitlement law".

All elephants have 4 legs, but not all things with 4 legs are elephants, and a tiger inside the elephant enclosure isn't "just" another animal.

9rx · 6h ago
> So there's obviously no such thing as "just an error" from that alone

The point is that all values are potentially errors. An age value, for example, can be an error if your business case requires restricting access to someone under the age of 18. There is nothing special about a certain value just because it has a type named "error", though.

Let's face it: At the root of this discussion is the simple fact that "if" statements are just not very good. They're not good for handling errors, but they're also not good for handling anything else either. It is just more obvious in the case of what we call errors because of frequency.

Something better is sorely lacking, but seeking better only for types named "error" misses the forest for the trees.

kiitos · 7h ago
You're simply wrong. If I call a function and it fails, then at the base level it doesn't matter if it failed because "no more disk space" or because "input values are invalid" -- the thing failed, in both cases. The caller needs to deal with that failure, in all cases. Now exactly how it deals with that failure might depend on properties of the error, sure, but the control flow of the program is the same in any case.
abtinf · 14h ago
> Its the thing I immediately add now that I have generics in Go.

If you’re willing to share, I’m very curious to see a code example of what you mean by this.

JamesSwift · 14h ago
This is from a while back but was the first thing I thought of: https://gist.github.com/J-Swift/96cde097cc324de1f8e899ba30a1...

I ripped most of it off of someone else, link in the gist

jitl · 7h ago
Moving every success value to the heap seems like a big loss to me but I don't see an alternative. I think going the interface route also ends up wrapping everything in an even fatter pointer. But at least I get to think "ah maybe this isn't going to get boxed and it will be free".

    interface Result[T] {
     IsOk(): bool
     IsErr(): bool
     Unwrap(): T
     UnwrapError(): error
    }
    
    // Ok is a Result that represents a successful operation.
    struct Ok[T] {
     Value: T
    }
    
    func Ok[T](value T) Result[T] {
     return Ok[T]{Value: value}
    }
    
    func (s Ok[T]) IsOk() bool {
     return true
    }
    
    func (s Ok[T]) IsErr() bool {
     return false
    }
    
    func (s Ok[T]) Unwrap() T {
     return s.Value
    }
    
    func (s Ok[T]) UnwrapError() error {
     panic("UnwrapError called on Ok")
    }
    
    
    // Err is a Result that represents a failed operation.
    struct Err[T] {
     Reason: error
    }
    
    func Err[T](reason error) Result[T] {
     return Err[T]{Reason: reason}
    }
    
    func (e Err[T]) Error() string {
     return e.Reason.Error()
    }
    
    func (e Err[T]) IsOk() bool {
     return false
    }
    
    func (e Err[T]) IsErr() bool {
     return true
    }
    
    func (e Err[T]) Unwrap() T {
     panic(fmt.Errorf("Unwrap called on Err: %w", e.Reason))
    }
    
    func (e Err[T]) UnwrapError() error {
     return e.Reason
    }
Pxtl · 13h ago
Wait...

  x, err := strconv.Atoi("123")
  if err != nil {
    panic(err)
  }
  y, err := strconv.Atoi("1234")
  fmt.Println("result:", x, y)
> this also compiles and runs just fine but again you would have no idea something was wrong

Okay, I don't use golang... but I thought ":=" was "single statement declare-and-assign".

Is it not redeclaring "err" in your example on line 5, and therefore the new "err" variable (that would shadow the old err variable) should be considered unused and fail with 'declared and not used: err'

Or does := just do vanilla assignment if the variable already exists?

kbolino · 13h ago
It's trickier than that, unfortunately. There has to be at least one new variable on the left side of := but any other variables that already exist in the same scope will simply be assigned to. However, if you use := in a nested block, then the variable is redeclared and shadows the outer-scope variable.
Pxtl · 13h ago
Thanks, I hate it.
recursivecaveat · 5h ago
This, along with stuff like implicit access modifiers based on case, nil interface confusion, named or multiple return values, channel lockups, and `append` oddities make it ring a bit hollow when people complain that no-one could be expected to google once and learn what snippet a `?` operator expands to. Like I don't even necessarily hate all these things, just please don't pretend it's the heart and soul of minimalism or approachable predictability.
JamesSwift · 13h ago
As I understand it, go has some special handling for this scenario because its so prevalent which special cases reassignment. The linked article touches on it

> There are exceptions to this rule in areas with high “foot traffic”: assignments come to mind. Ironically, the ability to redeclare a variable in short variable declarations (:=) was introduced to address a problem that arose because of error handling: without redeclarations, sequences of error checks require a differently named err variable for each check (or additional separate variable declarations)

Pxtl · 12h ago
... I thought Go's whole deal was that you give up the expressiveness and power of overdesigned languages for simple, clean, "only one way to do it" semantics. That "special cases reassignment" where ':=' is sometimes a shadowing declaration and sometimes a reassignment sounds like the opposite of that.
homebrewer · 12h ago
The language is full of gotchas like that, you're expected to use the tooling to guardrail yourself, because having a proper type system or coherent syntax is "too complicated" (while learning dozens of patterns and weird tricks apparently isn't).

go vet and this massive collection of linters bundled into a single binary are very popular: https://golangci-lint.run

linters will warn you of accidental shadowing, among many other things.

masklinn · 3h ago
> That "special cases reassignment" where ':=' is sometimes a shadowing declaration and sometimes a reassignment sounds like the opposite of that.

FWIW it is never a shadowing declaration. It is at least one non-shadowing declaration plus any number of reassignments.

The fun part is the tendency to keep reassigning to `err` makes the unused variable largely useless, so it’s just there to be a pain in the ass, and your need a separate lint anyway.

reader_1000 · 12h ago
> For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error.

Languages with stack traces gives this to you for free, in Go, you need to implement it every time. OK, you may be disciplined developer where you always augment the error with the details but not all the team members have the same discipline.

Also the best thing about stack traces is that it gives you the path to the error. If the error is happened in a method that is called from multiple places, with stack traces, you immediately know the call path.

I worked as a sysadmin/SRE style for many years and I had to solve many problems, so I have plenty of experience in troubleshooting and problem solving. When I worked with stack traces, solving easy problems was taking only 1-2 minutes because the problems were obvious, but with Go, even easy problems takes more time because some people just don't augment the errors and use same error messages which makes it a detective work to solve it.

pikzel · 13h ago
They still haven't solved shadowing.

  a, err := foo()
  b, err := bar()
  if err != nil { // oops, forgot to handle foo()'s err }
This is the illusion of safe error handling.
catlifeonmars · 5h ago
I’m surprised I don’t see this mentioned more. This is spooky action at a distance at its worst. And it’s not even limited to error handling. Any multi value assignment works like this.
AnimalMuppet · 13h ago
I would be astonished if there isn't an automated tool to check for that at the push of a button. I would be mildly surprised if there isn't a compiler flag to check for it.
arp242 · 13h ago
Not a compiler check, but staticcheck is widely used:

    % staticcheck test.go
    test.go:7:2: this value of err is never used (SA4006)
masklinn · 3h ago
There very much is not. There is a compiler error you can’t disable if a variable is completely unused and that is it.
dmitshur · 13h ago
Yeah, as one data point, https://staticcheck.dev/docs/checks/#SA4006 has existed since 2017.
_benton · 10h ago
It's fairly obvious when writing Go that `err` is being shadowed and needs to be checked after each expression. You should be wrapping them anyways!
tslocum · 13h ago
Seems strange that Rust's "?" gets a mention syntax-wise, but nothing is said about sum types coming to Go. Go's verbose error handling and lack of sum types are my only gripes with the language. It would be nice to see both addressed using Rust's Result type as a model.
hackingonempty · 14h ago
Generators and Goroutines have keywords/syntax in Golang but now they don't want to pile on more to handle errors. They could have had one single bit of syntactic sugar, "do notation", to handle all three and more if they had considered it from the beginning but it seems too late if the language designers are even aware of it. TFA says "If you’re wondering if your particular error handling idea was previously considered, read this document!" but that document references languages with ad-hoc solutions (C++, Rust, Swift) and does not reference languages like Haskell, Scala, or OCaml which have the same generic solution known as do-notation, for-comprehensions, and monadic-let respectively.

For example instead of

  func printSum(a, b string) error {
      x, err := strconv.Atoi(a)
      if err != nil {
          return err
      }
      y, err := strconv.Atoi(b)
      if err != nil {
          return err
      }
      fmt.Println("result:", x + y)
      return nil
  }
they could have something like this:

  func printSum(a, b string) result[error, unit] {
      return for {
          x <- strconv.Atoi(a)
          y <- strconv.Atoi(b)
      } yield fmt.Println("result:", x + y)
  }

which desugars to:

  func printSum(a, b string) result[error, unit] {
      return strconv.Atoi(a).flatMap(func(x string) result[error, unit] {
          return strconv.Atoi(b).map(func(y string) unit {
              return fmt.Println("result:", x + y)
          }
      }
  }
and unlike ad-hoc solutions this one bit of syntax sugar, where for comprehensions become invocations of map, flatMap, and filter would handle errors, goroutines, channels, generators, lists, loops, and more, because monads are pervasive: https://philipnilsson.github.io/Badness10k/escaping-hell-wit...
tayo42 · 8h ago
I don't think in real code you generally want to return "err" directly, but add some kind of context to made debugging easier. Its the same problem Rusts `?` has.

How do you do that with this suggestion?

VirusNewbie · 12h ago
I absolutely agree with this, and would be better than all of the other proposals.

Did anyone propose this in one of the many error handling proposals?

MeetingsBrowser · 2h ago
yes
MathMonkeyMan · 5h ago
One of Rob Pike's talks convinced me that the tendency to add features from other languages causes languages to all resemble each other. It's not a bad thing, but it's something to note. Consider the alternative: What if we had different languages for different tasks, new languages appearing from time to time, rather than accreting features onto existing general purpose languages every few years?

He also made the point that if you have two ways of coding something, then you have to choose every time. I've noticed that people disagree about which way is best. If there were only one way, then all of the same problems would be solved with the same amount of effort, but without any of the disagreement or personal deliberation.

Maybe Go should have exceptions beyond what panic/recover became, or maybe there should be a "?" operator, or maybe there should be a "check" expression, or some other new syntax.

Or maybe Go code should be filled with "if" statements that just return the error if it's not nil.

I've worked with a fair amount of Go code, but not enough that I am too bothered by which pattern is chosen.

On the other hand, if you spend more than a few weeks full time reading and editing Go code, you could easily learn a lot of syntax without issue. If you spend most of your career writing in a language, then you become familiar with the historical vogues that surrounded the addition of new language features and popular libraries.

There's something to be said for freezing the core language.

catlifeonmars · 5h ago
Small nitpick: panic/recover and error handling are orthogonal. Exceptions != errors.
ivanjermakov · 9h ago
No one here mentioned Zig approach there, so I'll share: https://ziglang.org/documentation/master/#try

Zig has try syntax that expands to `expr catch |e| return e`, neatly solving a common use case of returning an error to the caller.

tubthumper8 · 3h ago
The "Error Return Trace" described in the next section down also seems like an interesting alternative to a stack trace, possibly solving the same problem with less noise.

How well does it work in practice?

kristoff_it · 58m ago
It works really, really well, in part because it's also capable of tracking where an error is turned into another. And the error return trace ends where the stack trace begins (when an error is turned into a panic) so you don't even have to know what an error trace is, because it just makes sense intuitively.
joaohaas · 9h ago
Zig 'error types' are a sum type, while Go ones are just values.

And even then this is just the same as the '?' operator Rust uses, which is mentioned in the post.

Bratmon · 12h ago
Watching go people complaining about how other languages encourage bubbling errors up is always hilarious to me because there is literally nothing you can do with errors in go except bubble them up, log them, or swallow them.

Even the article considers "handling" an error to be synonymous with "Adding more text and bubbling it up"!

tail_exchange · 12h ago
> there is literally nothing you can do with errors in go except bubble them up, log them, or swallow them

You can also add additional context to the error before bubbling it up. But yes, that part of the point. Instead of bubbling them up, the programmer should instead reflect on whether it is better than just log and proceed, or completely swallow them. This is what error handling is about.

gwd · 12h ago
I mean, yeah, most of the time what you do is add more text and bubble up. But:

1. The very fact that adding more text isn't really any more verbose than not encourages you to add more text, making errors more informative.

2. A non-negligible amount of times you do something else: carry on, or do something specific based on what kind of error it was. For instance, ignore an error if it's in a certain class; or create the file if it didn't exist; and so on.

Forcing the error handling doesn't seem to me that different than forcing you to explicitly cast between (say) int and int64. Part of me is annoyed with that too, but then I have PTSD flashbacks from 20 years of C programming and appreciate it.

Mawr · 8h ago
I assume you mean languages with exceptions.

You can't bubble up an exception, it's done automatically. That's a very important distinction. You can't make the decision to bubble up or not, because you do not have the required information - you don't know whether an exception can be thrown or not at any point. Therefore, you can't say you're making a decision at all.

Explicit error allows you to be able to make the decision.

anttiharju · 13h ago
I was actually bit scared, so read through the whole post and am happy with the conclusion.

Go is very readable in my experience. I'd like to keep it that way.

lspears · 7h ago
Go’s restraint in adding new language features is a real gift to its users. In contrast, Swift feels like a moving target: even on a Mac Studio I’ll occasionally fail to compile a simple project. The expanding keyword list and nonstop churn make Swift harder to learn—and even harder to keep up with.
whstl · 14h ago
I used to hate the repetitive nature of Go’s error handling until I was burned by bad/mysterious error messages in production.

Now my error handling is not repetitive anymore. I am in peace with Golang.

However I 100% get the complaint from the people who don’t need detailed error messages.

ziml77 · 14h ago
They admit it's contrived, but this isn't very convincing

    func printSum(a, b string) error {
        x, err := strconv.Atoi(a)
        if err != nil {
            return fmt.Errorf("invalid integer: %q", a)
        }
        y, err := strconv.Atoi(b)
        if err != nil {
            return fmt.Errorf("invalid integer: %q", b)
        }
        fmt.Println("result:", x + y)
        return nil
    }
It's not adding anything that the Atoi function couldn't have reported. That's a perfect case for blindly passing an error up the stack.
nasretdinov · 13h ago
It does, it says which one of the two integers was incorrect
Zambyte · 12h ago
Why can't Atoi report that?
nasretdinov · 12h ago
I think Atoi actually does say that, but it's just a toy example. Most often functions outside the standard library don't contain the arguments in their error values
shermantanktop · 11h ago
"Not adding extra syntax is in line with one of Go’s design rules: do not provide multiple ways of doing the same thing"

And so any change to any existing functionality is a breaking change that invalidates all code which uses that functionality? That design rule smells like hubris on the part of Go's designers. It only works if all future changes are extensions and never amendments.

veggieroll · 14h ago
Error handling is one of my favorite parts of Go. The haters can rip `if err != nil { return fmt.Errorf("error doing thing: %w", err) }` from my cold dead hands.
catlifeonmars · 5h ago
How do you feel about

    v, err := foo.Open()
    // …
    defer func() {
        if closeErr := v.Close(); closeErr != nil {
            err = fmt.Errorf("while closing %w: %v", err, closeErr)
        }
    }() 
  
    // …

When you’re writing something trivial/pure, Go’s error handling is fine, if maybe a tad bit verbose, but it quickly becomes nightmarish when you start to do nontrivial things that are typical for systems programming.

FWIW I love Go, it’s my daily driver for most things. I still think it can get messy far too quickly

database64128 · 49m ago
Just write `defer v.Close()`? In almost all cases, `close(2)` errors can be safely ignored. Rust also does this: https://github.com/rust-lang/rust/blob/792fc2b033aea7ea7b766...
thayne · 10h ago
Error handling is the thing I hate the most about go. And none of the serious proposals I've seen would remove your ability to continue using `if err != nil`
veggieroll · 9h ago
I have yet to see a proposal that retains what I love about the status quo: conscientious error handling.

The language’s status quo forces everyone to think about errors more deeply than in other languages and acknowledges that the error case is as critical and worthy of the programmer’s attention.

thayne · 7h ago
Something like:

    x := FallibleFunction() ? err -> return fmt.Errorf("something happened %v", err)
Doesn't really change that, but significantly reduces the amount of noise from error handling boilerplate. And (most importantly to me) reduces the amount of vertical space taken up by error handling code, which makes it easier to follow the happy flow.
RVuRnvbM2e · 5h ago
Reduces the vertical space by vastly increasing the horizontal space and inserting line noise into the syntax. I don't even understand what that code would do.
sedatk · 9h ago
> forces everyone to think about errors more deeply than in other languages

Not really. Rust also forces you think deeply about errors but don't bother you with verbose syntax. I think Swift was also similar.

ummonk · 13h ago
You don't use gofmt?
tom_ · 11h ago
The whole point of gofmt is that you type in whatever you like and gofmt sorts it out for you. So if you're typing your code in a HN comment box, you'd surely just enter it roughly like that - though i recommend not even including the syntactically irrelevant spaces shown here. Your space bar does have a MTBF you know, and it's measured in activations... let gofmt take the strain. It can waste your screen space for you!

gofmt is the good bit about working in Go. Pretty much everybody uses it, and so you can use it too. Some other languages have similar tools, but they're not as pervasive, so it's far too easy to end up in a situation where you can't use the tool because it would just make too much of a mess of the inconsistently manually-formatted stuff that's already there.

rollcat · 14h ago
Snip:

    if err != nil {
        return fmt.Errorf("invalid integer: %q", a)
    }
Snip. No syntax for error handling is OK with me. Spelling out "hey I actually do need a stack trace" at every call site isn't.
skydhash · 14h ago
You actually don't. In mistakes #52 "Handling an error twice" from the book "100 Go Mistakes and How to Avoid Them" by Teiva Harsanyi, the recommendation is to either return an error to the caller or handle the situation (and maybe logging it). Sometimes you wnat some extra information as to why it's an error, so you wrap it.

Go forces you to be explicit about error handling. Java syntax is not that much better. JavaScript, Kotlin, Swift,... is more about avoiding null pointer exception than proper error handling.

rollcat · 13h ago
I return the error to the caller. The caller returns it to their caller. 5 frames up, someone gets a [syscall.EINVAL], and has to figure out what to do about it. Perhaps it's time to log it?

If I had to write my own "100 mistakes" book, "assuming the callee knows what to do" would be somewhere in the top 20, down below "I won't need to debug this".

skydhash · 12h ago
It's all about designing software. The callee is the one encountering the error, not any of the caller up in the stack trace. Somewhere in the call chain, there's a need to take a decision.

So you, as the developer, decide where that needs to be. It may be at the callee level (like an exponential retry) or at the caller level (display an error message). In the later case, you may want to add more information to the error data block, so that the caller my handle the situation appropriately. So if you want tracing, you just need to wrap the error and returns it. Then your logging code have all the information it needs: like

  [error saving file [permission error [can't access file]]]
instead of just [syscall.EINVAL].
rollcat · 12h ago
> So if you want tracing, you just need to wrap the error and returns it.

Here we go, fifth time we're both spelling this one out. This thread is now a meta-self-joke.

never_inline · 5h ago
> When debugging error handling code, being able to quickly add a println or have a dedicated line or source location for setting a breakpoint in a debugger is helpful.

On the flip side, you can't have exception breakpoints in Go.

arp242 · 14h ago
> Of course, there are also valid arguments in favor of change: Lack of better error handling support remains the top complaint in our user surveys.

Looking at that survey, only 13% mentioned error handling. So that means 87% didn't mention it. So in that sense, perhaps not too much weight should be given to that?

I agree the verbosity fades into the background, but also feel something better can be done, somehow. As mentioned there's been a gazillion proposals, and some of them seem quite reasonable. This is something where the original Go design of "we only put in Go what Robert, Ken, and Rob can all agree on" would IMHO be better, because these type of discussions don't really get a whole lot better with hundreds of people from the interwebz involved. That said, I wasn't a fan of the try proposal and I'm happy it didn't make it in the language.

And to be honest in my daily Go programming, it's not that big of a deal. So it's okay.

smw · 13h ago
"biggest challenge", it's not multiple choice
mparnisari · 11h ago
I have zero complaints about Go's error handling and I'm happy with the decision!
bccdee · 14h ago
Here's a non-syntactic suggestion. Could we just patch gofmt to permit this:

    if err != nil { return nil, err; }
as a well-formatted line of go? Make it a special case somehow.

My only big problem with if err != nil is that it eats up 3 lines minimum. If we could squish it down to 1, I'd be much more content.

GoatInGrey · 13h ago
The rub there is that you'll have varying styles in which error handling statements appear in your code. With simplistic instances appearing one way and less simplistic ones appearing similarly or differently depending on where each lands on the complexity spectrum. The idiomatic approach is for all instances to be written in the same way regardless of technical nuances.

All of that aside, I've come to learn that passing errors up the call stack without any wrapping or handling is a code smell. It is less than useless for me to attempt setting the value of cell A1 in an Excel sheet to "Foo" and then receive an out-of-range error because the developer made no attempt to even inform me of the context around the error. Let alone handling the error and attempting to correct state before giving up.

In my Excel example, the cause of the error was a data validation problem a couple columns over (F or so). The error was legitimately useless in troubleshooting.

mseepgood · 13h ago
How would you set a breakpoint on the error return case?
tubthumper8 · 3h ago
Inline breakpoint, the same way you set a breakpoint on an expression in any language
bccdee · 13h ago
You wouldn't. Rust's ? operator doesn't permit that either. If you need to put a breakpoint there, put a line break there.
mseepgood · 13h ago
One reason I consider Rust's approach worse than Go's.
verdverm · 14h ago
What if I wrap the error? Should that be squashed to one line? What is the heuristic?
bccdee · 13h ago
Sure, why not? Let the programmer decide if they want one line or three lines; the tool can permit both. Gofmt is line-length-agnostic—breaking up long lines is already considered to be the programmer's responsibility.
karel-3d · 11h ago
Hm, I would agree with this 2 years ago; but now I see how great both iterators and generics were despite my initial personal objections, so I am thinking we should give some less verbose error handling a chance. But alas
nvlled · 4h ago
> To be more precise, we should stop trying to solve the syntactic problem

Fixing the problem purely from a syntactic perspective avoids any unexpected semantic changes that could lead to irreconcilable disagreements, as clearly demonstrated by the infamous try proposal. Very simple syntactical changes that maps clearly to the original error handling has the advantage of being trivial to implement while also avoids having the developers needing to learn something new.

cherryteastain · 13h ago
All they have to do is to expand generics to support generic methods so we can have monadic operations like in C++'s std::expected or Rust's Result, like

    func (r Result[T, E]) AndThen[OtherT any](func(T) Result[OtherT, E]) Result[OtherT, E] { ... }
which would enable error handling like

    sum := 0
    parseAndAdd := func(s string) (func(string)Result[int, error]) { /* returns func which parses and adds to sum */ }
    return parseAndAdd(a)().AndThen(parseAndAdd(b))
There's a reason why every other language is converging to that sort of functional setup, as it opens up possibilities such as try-transform generics for ranges.
peterashford · 6h ago
I like Go. I use it at work and I find it pretty pragmatic.

But I can't stand the verbosity of the error handling. It drives me nuts. I also can't stand the level of rationalising that goes on when anyone dares to point out that Go's error handling is (obviously) verbose. The community has a pigheaded attitude towards criticism.

It also grinds my gears, because I really like that Go is in most other ways, simple and low on boilerplate. It's not unusual to see functions that are 50% error handling where the error handling actually DOES NOTHING. That's just insane.

melodyogonna · 13h ago
Go's error handling fade away after a month of consistent use, I would not risk backwards compatibility over this. In fact, I like the explicit error handling.
thayne · 10h ago
The proposals are not backwards incompatible. And just because you get used to something doesn't mean it is good.

And FWIW, my hatred of go error handling has not diminished with increased usage.

sedatk · 9h ago
> I like the explicit error handling

You mean "verbose error handling". All other proposals are also explicit, just not as verbose.

Neku42 · 10h ago
Even if the decision to close the door on this sucks I think they are correct - this is not a syntax problem. Adding sugar will not fix fundamental issues w/ Go's error handling.

They need to add/fix like 5-6 different parts of the language to even begin addressing this in a meaningful way.

zahlman · 14h ago
Watching the process of thinking about this from the outside, somehow reminds me of my experience on the inside of the Python community trying to figure out packaging.
ajkjk · 14h ago
Okay here's my idea, not found on the list in the article, what do you think:

You add a visualization sugar via an IDE plugin that renders if/else statements (either all of them or just error cases) as two separate columns of code --- something like

    x = foo(); 
    if (x != nil)        | else
      <happy case>       | <error case>
And then successive error cases can split further, making more columns, which it is up to the IDE to render in a useful way. Underneath the representation-sugar it's still just a bunch of annoyingly nested {} blocks, but now it's not annoying to look at. And since the sugar is supported by the language developers, everyone is using the same version and can therefore rely on other developers seeing and maintaining the readability of the code in the sugared syntax.

If the error case inside a block returns then its column just ends, but if it re-converges to the main case then you visualize that in the IDE as well. You can also maybe visualize some alternative control flows: for instance, a function that starts in a happy-path column but at all of its errors jumps over into an error column that continues execution (which in code would look like a bunch of `if (x=nil) { goto err; }` cases.

Reason for doing it this way: logical flow within a single function forms a DAG, and trying to represent it linearly is fundamentally doomed. I'm betting that it will eventually be the case that we stop trying to represent it linearly, and we may as well start talking about how to do it now. Sugar is the obvious approach because it minimizes rethinking the underlying language and allows for you to experiment with different approaches.

layer8 · 13h ago
I think that IDE functionality is fine for writing code, but shouldn’t be imposed for the UX of reading code, because code reading also happens a lot outside of IDEs, because it constrains the choice of editors, and because it creates fundamentally different “modes” of source code presentation. The visualizations will start to appear in comment threads like this one, and in other publications on the web, but copying them and pasting them into an editor will not work (will be invalid syntax). It creates unnecessary complications across the whole ecosystem. Language syntax should stand on its own, and shouldn’t need crutches like that to make it ergonomic to read.
ajkjk · 13h ago
I have the opposite opinion, I guess! We've been trying to solve everything with language syntax for a long time and it's a bit of a dead end, as the OP shows. Better to start trying new things.

Anyway you can always copy paste it in the normal linear format.

Someone · 13h ago
I would simplify that to

  x = foo() ||| <error case>
  <happy case>
(With the specific symbol used in lieu of ||| to be decided)

That is shorter and keeps the happy path unindented, even if it has additional such constructs, for example

  x = foo() ||| return Error(err, “foo failed”)
  y = bar() ||| return Error(err, “bar failed”)
latchkey · 12h ago
Gauntlet, the CoffeeScript of golang, seems to have an interesting approach:

https://gauntletlang.gitbook.io/docs/advanced-features/try-s...

skissane · 5h ago
Another way of potentially addressing this problem: add some kind of macro facility to Go – maybe take inspiration from Rust

This could be used to solve both "syntactic support for error handling", and also various other common complaints (such as lack of first class enums), without "polluting" the core language – they'd be optional packages which nobody would have to use if they don't want to

Of course, if one of these optional packages ever became truly prevalent, you could promote it to the standard library... but that would involve far less bikeshedding, because it would be about making the de facto standard de jure... and arguably, letting people vote with their feet is a much more reliable way of establishing consensus than any online discussion could ever be

abtinf · 14h ago
IMHO, the actual problem with go error handling isn’t the error handling at all — it’s that multiple return values aren’t a first class construct. With proper tuple handling and Go’s approach to generics, a lot of these issues would just disappear.
JamesSwift · 14h ago
This also grinds my gears when converting multiple-return functions to returns-a-channel functions. Generics help with that now.
keyle · 7h ago
It saddens me that the conclusion is

      "it's been like this for this long now" 
and

      "no one could ever agree to something" 
leads to this amount of verbosity. Any of these keywords approach, `try`, `check`, or `?` would have been a good addition, if they kept the opportunity to also handle it verbosely.

The argument that LLM now auto-completes code faster than ever is an interesting one, but I'm baffled by such an experienced team making this an argument, since code is read so many more times than it is written; they clearly identify the issue of the visual complexity while hand-waving the problem that it's not an issue since LLM are present to write it - it completely disregards the fact that the code is read many more times that it is written.

Visual complexity and visual rhythms are important factors in a programming language design, I feel. Go is excruciatingly annoying to read, compared to Dart, C, or any cleaner language.

Ultimately, it's just one of those "meh, too hard, won't do" solution to the problem. They'll write another one of those posts in 5 years and continue on. Clearly these people are not there to solve user problems. Hiding behind a community for decision making is weak. Maybe they should write some real world applications themselves, which involves having these error checks every 2nd lines.

At this point I wouldn't be upset if someone forked Go, called it Go++ and fixed the silly parts.

unclad5968 · 14h ago
I don't use Go, but I actually like Go's error handling and I think multiple return values is a better solution than any other language I've used. So much so, I've basically adopted it in my c++ code using std::pair. Errors are a value, and the abstraction over that is unnecessary in my opinion. Rust's result type is just syntactic sugar around the multiple return value approach. I don't care for the syntactic sugar, and doing many things in few lines of code isn't valuable to me, but I suspect this is why people love rust's error handling.
hmry · 13h ago
> Rust's result type is just syntactic sugar around the multiple return value approach

That's really not true. Multiple return values means you always need to return some return value and some error value, even if they are dummy values (like nil). While a result type / sum type genuinely only contains one branch, not the other.

If you had a language that didn't have nil, it would genuinely be impossible to emulate sum type like behavior on top of multiple return values. It serves as an escape hatch, to create a value of some type when you don't actually have a meaningful value to give.

std::variant / std::expected / std::optional aren't syntactic sugar for std::pair either.

parentheses · 7h ago
The fundamental thing that try/catch and similar language structures give us is an error _boundary_. I feel that not having one is a weakness of the language. The nesting and syntax all serve the purpose of handling errors neatly. I think it works very well and Go has unsolved the problem.

I am not saying that the mechanism is perfect but it is more useful if we have it than not. IMO it's only weakness is that you never know if a new exception type is thrown by a nested function. This is a weakness for which we really don't have a solid solution - Java tried this with checked exceptions.

Go not using such a paradigm to me is bonkers. Practically every language has such a construct and how we use it best is pretty much convention these days.

eximius · 13h ago
Ah, shame. `foo := someFallibleMethod()?` would have been nice for when you don't want to handle/wrap the error.

I'm not super strongly against the constant error checking - I actually think it's good for code health to accept it as inherent complexity - but I do think some minor ergonomics would have been nice.

JyB · 11h ago
This is such a great move. Faith restored in the language after the generics debacle.
agluszak · 10h ago
> generics debacle

Why debacle?

nilirl · 14h ago
Error handling is some of the least fun parts of writing code. In all languages.

But in any case, why so much fear of being wrong?

> we have fine-grained control over the language version via go.mod files and file-specific directives

And that may be the real truth of it: Error handling in Go just isn't ... that much of a problem to force action?

dlock17 · 6h ago
This is also my feeling. And I wouldn't even have minded the ? syntax.

It seems that now that Ian's left the rest of the Go team is just being honest about what they are willing to spend their time on.

And I'm more than fine with that, because look at this comment section. You can't please everybody.

scoodah · 13h ago
> And that may be the real truth of it: Error handling in Go just isn't ... that much of a problem to force action?

I you are right that this is the truth of it. Error handling just isn’t that big a problem. 13% reported it on the survey cited. That doesn’t seem that significant. And honestly, after writing Go, I barely notice error handling as I’m writing/reading code anyway. If anything I appreciate it a bit more than exceptions.

Always something that can be complained about. But it doesn’t mean every complaint is a huge deal.

bravesoul2 · 11h ago
Slowly reinventing exceptions seems to go against the spirit of Go. That is what you read is what you get.

Haskell solves this with the do notation, but the price is understanding monads. Go also aims to be easy to understand.

thayne · 10h ago
The proposals aren't reinventing exceptions. They are just making more ergonomic syntax for doing the same thing go error handling does today.
bravesoul2 · 9h ago
You can argue that is what an exception is. A more ergonomic way to deal with errors that handles propagation for you.
throwaway71271 · 14h ago
Everything is fine

I dream if err, if err dreams me.

dcow · 8h ago
Go’s philosophy is the problem: sometimes you need a few ways to do the same thing. And that’s okay. Sometimes the flow you’re trying to express in code just needs the error to go up and get handled at the top. Sometimes you want to act on it. Right now all you can do is act on it. So all the times you don’t need to act on it have to suffer? I call profuse bullshit. There is 100% a way to add terse compiler-enforced error handling (for the love of all things holy if nothing at all changed with the syntax at least there should be consensus that it’s a serious problem that the compiler can’t tell you “oops you forgot to handle that error”) to be added in a way that doesn't bifurcate API surfaces. You just have to do it even in the face of dissent. You have to compromise on your values for the good of the language. What is the damn problem with ? literally expanding to `if err != nil { \n return err \n }`? Just take small steps that improve everyone’s life. If you look for excuses not to improve error handling you’ll find them. Sadly that appears to be what has happened here. The entire language suffers because a vocal minority are sadists. Is it time for me to come back to Go? I think not.
sedatk · 10h ago
I'm so happy that Rust sorted this out way early in its lifetime. Error handling is so nice there.
tick_tock_tick · 10h ago
Rust is basically someone looking at ML languages and thinking what if we did that but just worse in every way.
codehakase · 14h ago
Can't believe we're going to get GTA 6 before an agreed upon (cleaner) error handling pattern in Go.
stefanos82 · 15h ago
I have hard time understanding why they didn't go with

    func printSum(a, b string) error {
        x := strconv.Atoi(a) or {
            return error
        }
        y := strconv.Atoi(b) or {
            return error
        }
        fmt.Println("result:", x + y)
        return nil
    }

or something along these lines...
righthand · 15h ago
> Unfortunately, as with the other error handling ideas, this new proposal was also quickly overrun with comments and many suggestions for minor tweaks, often based on individual preferences. Ian closed the proposal and moved the content into a discussion to facilitate the conversation and to collect further feedback. A slightly modified version was received a bit more positively but broad support remained elusive.

> After so many years of trying, with three full-fledged proposals by the Go team and literally hundreds (!) of community proposals, most of them variations on a theme, all of which failed to attract sufficient (let alone overwhelming) support, the question we now face is: how to proceed? Should we proceed at all?

> We think not.

arp242 · 15h ago
You need to assign the error to a variable, probably. So it would have to be more something like:

  n := strconv.Atoi(s) or |err| {
      return fmt.Errorf("foo: %w", err)
  }

  n := strconv.Atoi(s) or |err| {
      if !errors.Is(err, pkg.ErrFoo)
          return fmt.Errorf("foo: %w", err)
      }
  }
Just "error" (which shadows the built-in type) won't really work.

I'm just making up syntax here to illustrate the point; doesn't look too brilliant to me. A func might be a bit more "Go-like":

  n := strconv.Atoi(s) or func(n int, err error) {
      return fmt.Errorf("foo: %w", err)
  }
All of this is kind of a moot point at Robert's blog post says that these proposals won't be considered for the foreseeable future, but IMHO any error handling proposal should continue to treat errors as values, which means you should be able to use fmt.Errorf(), errors.Is(), mylogger.Error(), etc.
alanbernstein · 15h ago
Seems like it's only worth the cost of the change if it removes all three verbose lines per check, this only removes one.
shivamacharya · 15h ago
If you do this, returning the error last is now part of the language rather than a convention. You’d be making a pretty large language change.. and for what? One line of code that’s already folded away by any modern editor?
wredcoll · 15h ago
Oh cool, you've reinvented perl.
danenania · 14h ago
I have no problem with Go’s error handling. It’s not elegant, but it works, and that’s very much in keeping with the pragmatic spirit of Go.

I’m actually kind of surprised that it’s the top complaint among Go devs. I always thought it was more something that people who don’t use Go much complain about.

My personal pet issue is lack of strict null checks—and I’m similarly surprised this doesn’t get more discussion. It’s a way bigger problem in practice than error handling. It makes programs crash in production all the time, whereas error handling is basically just a question of syntax sugar. Please just give me a way to mark a field in a struct required so the compiler can eliminate nil dereference panics as a class of error. It’s opt-in like generics, so I don’t see why it would be controversial to anyone?

CactusRocket · 14h ago
I have 2 problems.

It's too easy to accidentally write `if err == nil` instead of `if err != nil`. I have even seen LLMs erroneously generate the first instead of the latter. And since it's such a tiny difference and the code is riddled with `if err != nil`, it's hard to catch at review time.

Second, you're not forced by the language to do anything with the error at all. There are cases where `err` is used in a function that not handling the `err` return value from a specific function silently compiles. E.g.

    x, err := strconv.Atoi(s1)
     if err != nil {
      panic(err)
     }
     y, err := strconv.Atoi(s2)

    fmt.Println(x, y)


I think accidentally allowing such bugs, and making them hard to spot, is a serious design flaw in the language.
danenania · 14h ago
I guess those are fair criticisms in the abstract, but personally I can’t recall a single time either has caused a bug for me in practice. I also can’t ever recall seeing an LLM or autocomplete mess it up (just my personal experience—I’m sure it can happen).
masklinn · 14h ago
> It’s opt-in like generics, so I don’t see why it would be controversial to anyone?

It "breaks" the language in fundamental ways — much more fundamental than syntactic sugar for error handling — by making zero values and thus zero initialisation invalid.

You even get this as a fun interaction with generics:

    func Zero[T any]() T {
        var v T
        return v
    }
danenania · 14h ago
I don’t see how it breaks anything if it’s opt-in. By default you get the current behavior with zero value initialization if that’s what you want (and in many cases it is). But if you’d rather force an explicit value to be supplied, what’s the harm?
masklinn · 14h ago
> I don’t see how it breaks anything if it’s opt-in?

Zero values are a fundamental, non-optional, "feature" of Go.

> But if you’d rather force an explicit value to be supplied, what’s the harm?

What happens if you use the function above with your type?

Or reflect.Zero?

danenania · 13h ago
If it would only complain on struct literals that are missing the value (and force a nil check before access if the zero value is nil to prevent panics), that would be enough for me. In that case, your Zero function and reflect.Zero can keep working as-is.
masklinn · 13h ago
Then I fail to see the point, that is trivial to lint, just enable the exhauststruct checker.
danenania · 13h ago
I wasn't aware of it—will check it out, thanks.
ddoice · 14h ago
Oh No! Is a much better name for error handling.
arccy · 14h ago
considering the recent post [1] on system correctness in amazon quoting "In 2014, Yuan et al. found that 92% of catastrophic failures in tested distributed systems were triggered by incorrect handling of nonfatal errors."

perhaps it's a good thing that error handling is so explicit, and treated as a regular code path.

[1]: https://news.ycombinator.com/item?id=44135638

renewiltord · 13h ago
I am very entertained by this. The Golang community bikeshedded their way into the status quo. Hahaha, I have to say that's a pretty good move by the steering org. Punishes bikeshedding.
dlock17 · 6h ago
This is a very good way to fight back against design by committee.
pphysch · 9h ago
It could be seen as a big waste of time, but it sets a good precedent.
agumonkey · 13h ago
makes you miss the `or die` idiom :)
cyberax · 5h ago
Sigh. They failed.

Dudes, error handling is THE worst part of Go. All of it.

And not just the lack of the "?" operator. It's also the lack of stacktraces, the footguns with nils-that-are-not-nil, problems with wrapping, the leakage of internal error messages to users (without i18n).

Literally everything. And you're just shutting down even _discussions_ of this?

stackedinserter · 13h ago
IDE's could help and just hide standard `if err != nil return fmt.Errorf("sh: %w", err)` blocks for us, or show them in some distinct way, like question mark after the statement.
te_chris · 14h ago
The way elixir conventionally uses tuples and pattern matching is really good.
827a · 15h ago
IMO: The position of "we aren't sure what the right approach to improvement is so we aren't going to do anything" has killed far bigger and more important projects, companies, and even countries, than Golang. Adapt or die; the world isn't going to wait for you.

(I love the Go team, and appreciate everything they do. I'm just sad to see a language I used to love fail to keep pace with many of the other options out there today.)

arp242 · 14h ago
> Adapt or die

It's not my impression Go is dying. Seems rather overblown.

And this "but $other_lang has it! You must have it! Adapt or die!" type of reasoning is how you end up with C++.

827a · 13h ago
I never said Go is dying. I said go must adapt, or it will die. That's future-tense.

Sure, you can end up with C++ (which is still by some measures the most popular programming language in the world, so that's not a bad place to be). You can also end up with Rust, or Kotlin, or any one of the literally every other programming languages in any ranking's Top 30, all of which have more ergonomic error handling.

A better example in the opposite direction is Java: Its a language that spent years refusing to adapt to the ever-changing needs of software engineers. Its legacy now. That is not Go's present, but it is Go's future at its current pace. Still powering a ton of projects, but never talked about except in disdain like "ugh that Go service is such tech debt, can we get time modernize it next sprint". I don't want that for the language.

MeetingsBrowser · 15h ago
Changing too quickly is a much bigger problem. It may not be ideal, but I think leaning towards being slow makes sense in this context.

Every person/company using Go chose to use it knowing how errors are handled.

Each new way of error handling seems to upset a large number of users, some of which may not have chosen Go had the newer system been in place originally.

If it is impossible to know which choice is correct, at least everyone has some baseline level of acceptance for the status quo.

827a · 13h ago
Changing too quickly is not a problem. Changing too quickly can lead to problems.

I don't agree that the problems it leads to are bigger problems than stagnation. I also don't believe they're smaller problems; sorting the problems by size is intractable, as it is situation dependent.

The challenge is in the definition of "too quickly"; if fifteen years of stagnation in addressing more productive error handling is the "right pace" of innovation, or lack-there-of; is twenty years? Thirty years? One hundred years? How do you decide when the time is right? Is the Go team just waiting out the tides of the Vox Populi, and maybe one day a unified opinion from the masses will coalesce?

That's weak.

MeetingsBrowser · 2h ago
Is it really fair to say a language is stagnating if it does not re-invent itself every ten years to match whatever language features are popular at the time?

> How do you decide when the time is right?

When people are migrating away from Go because of the error handling.

> maybe one day a unified opinion from the masses will coalesce?

Maybe. What is the alternative? If there are five alternative error handling proposals each with support from 20% of users, should they pick one at random and upset 80% no matter what?

justin66 · 15h ago
Go adoption has only increased over time.
827a · 12h ago
There's no methodologically sound way to measure its popularity available to most people to make an argument on its popularity one way or the other. That's why I didn't, and that's why you shouldn't either.
justin66 · 10h ago
If you say so, but the concern you floated (The position of "we aren't sure what the right approach to improvement is so we aren't going to do anything" has killed far bigger and more important projects, companies, and even countries, than Golang. ) is fundamentally silly, given how long Go has been in business and seemingly thriving while applying this approach to a few select, controversial features.

If it was going to be killed by this approach, it would now be dead.

acedTrex · 15h ago
In this particular case the argument seems to be that there is not even consensus improvement is NEEDED. If you cant even agree on that then how do ever hope to agree to a change in the general sense.
honkycat · 14h ago
awesome I love noisy boilerplate in my code, it isn't annoying at all
marcoc · 15h ago
Now that I think about it, are there any color schemes or extensions that highlight the error handling logic differently so that one can better focus on the “main” logic flow of the code while the error handling logic is still there?
rollcat · 14h ago
You would have to infer the intent of the code. One example, from a small project I'm working on:

    $ find . -name "*.go" | xargs grep 'if err !=' | wc -l   
     242
    $ find . -name "*.go" | xargs grep 'if err ==' | wc -l
      12
So about 5% of the error checking code is about handling the edge cases, where we are very much interested in what the error actually is, and need to handle those conditions carefully.

If you discard that as "error handling noise", you're in for a bug. Which is, by the way, perhaps the worst side-effect of verbose, repetitive error handling.

Apropos syntax highlighting: many themes in regular use (certainly most of the defaults) choose a low-contrast color for the comments. The comments are often the most important part of the code.

Refreeze5224 · 15h ago
I would love something like this, and if it exists, I've not come across it. Offloading a way of differentiating error handling syntax vs. normal code to the IDE seems like a nice way of handling this issue.
renewiltord · 14h ago
Goland folds it visually partially https://github.com/golang/vscode-go/issues/2311
zb3 · 14h ago
Thankfully repetitive language is less of a problem now that we have AI. The current syntax is just "add error handling to this function" :)
homebrewer · 14h ago
Writing it was never the problem if you're using proper tools, e.g. an actual IDE (IDEA does fine), or at least a snippet manager for your text editor. Inserting a wrapping error handling snippet requires two key presses.

It's reviewing mountains of that crap that's the problem, especially if there are non-trivial cases hidden in there, like returning the error when `err == nil` (mentioned by others in this thread).

Jtsummers · 14h ago
You're getting downvoted, but this was what tptacek basically wrote about. Key points from his blog are that LLMs are good at tedium, and Go's structure is highly repetitive and amenable to LLM generation. The error handling tedium is probably part of why it's highly repetitive.

> I work mostly in Go. I’m confident the designers of the Go programming language didn’t set out to produce the most LLM-legible language in the industry. They succeeded nonetheless Go has just enough type safety, an extensive standard library, and a culture that prizes (often repetitive) idiom. LLMs kick ass generating it.

https://news.ycombinator.com/item?id=44163063 - 2386 comments

rco8786 · 15h ago
This feels like it's making it worse lol
klabb3 · 12h ago
I love Go, but this is almost farcically hilarious:

> The goal of the proposal process is to reach general consensus about the outcome in a timely manner. If proposal review cannot identify a general consensus in the discussion of the issue on the issue tracker, the usual result is that the proposal is declined.

> None of the error handling proposals reached anything close to a consensus, so they were all declined.

> Should we proceed at all? We think not.

The disconnect here is of course that everyone has opinions and Google being design-by-committee can’t make progress on user-visible changes. Leaving the verbose error handling is not the end of the world, but there’s something here missing in the process. Don’t get me wrong, I love inaction as a default decision, but sometimes a decision is better than nothing. It reminds me of a groups when you can’t decide what to have for dinner – the best course of action isn’t to not eat at all, it’s to accept that everyone won’t be happy all the time, and take ownership of that unhappiness, if necessary, during the brief period of time when some people are upset.

I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go. IME these stylistic preferences are as easily abandoned as they are acquired.

To put another way: imagine if gofmt was launched today. It would be absolutely impossible to release through a consensus based process. Just tabs vs spaces would be 100 pages on the issue tracker of people willing to die on that hill. Yet, how many people complain about gofmt now that it’s already there? Even the biggest bike shedders enjoy it.

derefr · 11h ago
To take this analysis another level deeper: what has happened here is a classic example of bikeshedding — and, worse, fostering bikeshedding.

Everyone feels equipped to have an opinion about "what should be the syntax for an obvious bit of semantics." There's no expertise required to form such an opinion. And so there are as many opinions on offer as there are Go developers to give them.

Limit input on the subject to just e.g. the people capable of implementing the feature into the Go compiler, though, and a consensus would be reached quickly. Unlike drive-by opinion-havers, the language maintainers — people who have to actually continue to work with one-another (i.e. negotiate in an indefinite iterated prisoner's dilemma about how other language minutiae will work), are much more willing to give ground "this time" to just move on and get it working.

(Tangent: this "giving ground to get ground later" is commonly called "horse trading", but IMHO that paints it in a too-negative light. Horse trading is often the only reason anything gets done at all!)

bicarbonato · 10h ago
> I love inaction as a default decision

The thing is, inaction is not simply "not taking an action"; Inaction is taking active action of accepting the current solution.

> I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go.

But people may leave go if they constantly avoid fixing any of problems with the language. The more time passes, the more unhappy people become with the language. It will be a death by a thousand cuts.

I love go. But their constant denial do fix obvious problems is tiring.

imiric · 9h ago
> But people may leave go if they constantly avoid fixing any of problems with the language.

For many people the current Go error handling just isn't a problem. Some even prefer it over the overengineered solutions in other languages. This brutalist simplicity is a core design feature I personally enjoy the most with Go, and forcing me to use some syntax sugar or new keywords would make it less enjoyable to use. I also don't think that generics were a net benefit, but at least I'm not forced to use them.

Go is a cemented language at this point, warts and all. People who don't like it likely won't come around if the issues they're so vocal about were fixed. It's not like there's a shortage of languages to choose from.

zaptheimpaler · 11h ago
Yeah this bugs me about the decision process too, but Go places more importance on backwards compatibility and stability than most other languages so it does align with their values. I'm not the biggest fan of Go but its nice having a language around that favors simplicity and stability.
Mawr · 10h ago
> I love inaction as a default decision, but sometimes a decision is better than nothing. It reminds me of a groups when you can’t decide what to have for dinner – the best course of action isn’t to not eat at all, it’s to accept that everyone won’t be happy all the time, and take ownership of that unhappiness, if necessary, during the brief period of time when some people are upset.

Invalid comparison - eating one foodstuff or another affects a few people for a few hours. Significantly changing a popular language affects every single user of it forever.

mseepgood · 11h ago
> sometimes a decision is better than nothing

Not in this case. The most popular Go proposal/issue of all times was 'leave "if err != nil" alone': https://github.com/golang/go/issues?q=is%3Aissue%20%20sort%3...

thayne · 10h ago
If go had started out having different syntax for error handling, would these same people request that it change to what go currently does? Or is this just resistance to change, and wanting to keep what they are used to?

I suspect it is the latter.

mseepgood · 10h ago
The second most popular issue was adding generics. So it's probably not a resistance to change.
bloppe · 10h ago
The article seems to admit that a `try` keyword restricted to statements / assignments only would combine the best parts and alleviate the major concerns of both the `try` and `?` proposals. It reads as though the concept has not been seriously discussed due simply to exhaustion.
sho_hn · 10h ago
Pro and con. It hinders progress, but it can also arrest the "enshittification" of a language that takes place when it slowly adds complexity and features and support for whole paradigms.

Python does offer a lot more utility for the expert these days, but it also went from the maxim of "There is one obvious way to do it" to having 5-6 ways to format strings, adding more things to be familiar with, causing refactoring churn as people chase the latest way to do it, etc.

I'm a C++ developer. I wouldn't want to go back to older versions of the language, but it's also very hard to recruit any newer programmers into using it willingly, and the sheer amount of stuff in it that exists concurrently is a big reason why.

bloppe · 8h ago
I agree with you in principle, but this isn't like T-strings or C++ concepts. We're talking about a feature that has become infamous for it's lack of syntax in Go. A feature that every other major language has some syntax for.
VirusNewbie · 11h ago
It's true that Google is "design by committee" and consensus driven, but the Go team has been particularly obtuse about things, seemingly not understanding which opinions are valid and which are confused.
TZubiri · 13h ago
Love it, focus on building with what we have.

Resist enshittification, the greatest advantage in foundational software is sometimes saying no to new features.

evmar · 14h ago
Most discussions of language features immediately fall into the politician's syllogism:

https://en.wikipedia.org/wiki/Politician%27s_syllogism

I appreciate the Go language's general sense of conservatism towards change. Even if you're not a fan of it, I think it's admirable that there is a project staking out a unique spot in the churn-vs-stability design space. There are plenty of other projects that churn as fast as they can, which also has its pros and cons, and it's great to be able to see the relative outcomes.

PS: it's kind of hilarious how the blog post is like "there are hundreds of proposals and miles of detailed analysis of these", vs the commenters here who are like "I thought about this for five minutes and I now have an idea that solve everything, let me tell you about it".

mseepgood · 14h ago
Sometimes doing nothing is the right thing to do. (Quote from Until Dawn)

Go chose not to change the error handling - Nature remained in balance.

ummonk · 14h ago
I'd happily come up with criticisms of any specific proposal and bikeshed it, but any one of these proposals would be preferable to the status quo.

I'd understand if they decided they needed more time to continue iterating on and analyzing proposals to find the right solution, but simply declaring that they'll just suspend the whole effort because they can't come to a consensus is rather infuriating.

sa46 · 13h ago
“Simply declaring” is inaccurate description of the Go team’s decision. The team built several proposals, reviewed dozens more, and refined the process by gathering user feedback in multiple channels.

The Go team thoroughly explored the design space for seven years and did not find community consensus.

ummonk · 13h ago
There are two possibilities.

1) There isn't consensus that improved syntax for error handling is needed in the first place. If that is the case, they should just say so, instead of obfuscating by focusing on the number of proposals and the length of the process.

2) There is consensus about a need for improved error handling syntax, but after seven years of proposals they haven't been able to find community consensus about the best way to add said syntax. That would mean that improved syntax for error handling is necessary, but the Go team is understandably hesitant to push forward and lock in a potentially inferior solution. If that is the case, then would be reason to continue working on improved syntax for error handling, so as to find the best solution even if it takes a while.

Refreeze5224 · 15h ago
I don't really understand this decision. They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue. Seeking the perfection of syntax that everyone agrees on seems to be the enemy of providing some good alternative to the status quo.

Their comment about providing some new syntax and people being forced to use it seems off base to me. It's nice to not have multiple ways of doing things, but having only 2 when it comes to error handling does not seem like a big deal. I imagine people will just use their preference, and a large percentage of people will have a less verbose option if they want it.

munificent · 14h ago
> They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue.

Agreement on a problem does not imply agreement on a solution.

It's not about perfection. It's about not having a solution that gets anywhere near a majority approval.

Let's say your neighborhood has an empty plot of land owned by the city that is currently a pile of broken chunks of concrete, trash, and tangled wire. It's easy to imagine that there is unanimous agreement by everyone in the neighborhood that something better should be placed there.

But the parents want a playground, the pet owners want a dog park, the homeless advocates want a shelter, the nature lovers want a forest, etc. None of them will agree to spend their tax dollars on a solution that is useless to them, so no solution wins even though they all want the problem solved.

Grikbdl · 14h ago
Even if people in your example couldn't agree on a particular alternative, the outcome still is a less attractive area, maybe some will move out and fewer people move in. So, any solution would be better than the status quo - and they all would probably agree on that.

The lack of a good error handling story to a lot of people puts go in a mental trash bin of sorts. Similar (but different) reasons eg Java goes to a mental trash bin. I think leaving this issue unhandled will only make go look worse and worse in comparisons as the programming language landscape evolves. It might take 10 or 20 years but it'll always be unique in having "trash bin worthy" error handling. (this can perhaps be argued - maybe exceptions are worse, but at least they're standard).

munificent · 13h ago
> So, any solution would be better than the status quo - and they all would probably agree on that.

The point is that people do not agree that any solution is better than the status quo. In my analogy, if redeveloping that plot of land is quite expensive in tax dollars, people will prefer it be left alone completely so that money can be spent elsewhere than have it squandered on a "solution" that does nothing for them.

Likewise in Go, adding language features has a quite large cost in terms of cognitive load, decision load, implementation cost, etc. After many many surveys and discussions, it's clear that there is no consensus among the Go ecosystem that any error handling strategy is worth that cost.

throwaway894345 · 12h ago
In the analogy we might suppose everyone agrees that there is a problem and any solution is better than the status quo, but that's extremely unlikely in the case of Go. In my experience discussing this issue with Go users and critics, a lot of Go users find the status quo to be minimally chafing.

> The lack of a good error handling story to a lot of people puts go in a mental trash bin of sorts. ... It might take 10 or 20 years but it'll always be unique in having "trash bin worthy" error handling. (this can perhaps be argued - maybe exceptions are worse, but at least they're standard).

Remember that the context is syntactic error handling proposals, not proposals for error handling generally--the maintainers are saying they're only going to close syntax-only error handling proposals. While I have no doubt that there are lots of people who write of Go for its error handling syntax alone, I don't see any reason why a language community should prioritize the opinions of this group.

Additionally, while I have plenty of criticism for Go's error handling, I can't take "Go's error handling is 'trash bin worthy'" seriously. There are no languages that do error handling well (by which I mean, no implicit control flow and one obvious way to create errors with appropriate error context, no redundant context, clear error messages, etc). Go and Rust probably both give you the tools necessary to do error handling well, but there's no standard solution so you will have issues integrating different libraries (for example, different libraries will take different approaches to attaching error context, some might include stack traces and others won't, etc). It's a mess across the board, and verbosity is the least of my problems.

pixl97 · 12h ago
>It's about not having a solution that gets anywhere near a majority approval.

You'll never get it in any non-gamed environment.

In democratic voting in FPtP systems if there isn't a majority winner you'll take the top two and go to runoffs forcing those that are voting to pick the best of the bad choices.

This is the same thing that will typically happen in the city you're talking about, hence why most democracies are representative and not direct.

arp242 · 14h ago
> They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue.

According to 13% of respondents. So yes, it's the "#1 issue", but also not by a huge overwhelming majority or anything.

pixl97 · 12h ago
Honestly this is only because they made a bad survey. A ranked choice is better.

Lets say you have 5 choices. You give each choice a voting weight of 1 (not an issue) to (5 biggest issue). You only get to pick a weight once.

So in this type of voting even if everybody put error handling and #4 it could still win by a large margin if the 5 values were spread out over other concerns.

nepthar · 14h ago
Yeah, this is the single biggest reason I avoid go - I just don't want to clutter my "happy path" logic. It makes things harder to reason about.

"Errors are values", sure. Numbers are values and Lists are values. I use them differently, though.

I wonder if there could be "stupid" preprocessing step where I could unclutter go code, where you'd make a new token like "?=", that got replaced before compile time. For instance, "x ?= function.call();" would expand to "x, err := function.call(); if (err != nil) return err;"

skydhash · 13h ago
There's no happy path in programming. Errors are just some of the many states in the code and the transition to them doesn't disappear magically because you chose to not specify them. Actually returning an error is just a choice, you can chose to handle the situation, maybe log the error and go on your merry way. Or you panic() and cancel the whole call stack back to a recover(). I like Go because it forces you to be explicit.
peterashford · 6h ago
There absolutely is a happy path in programming - what you want the code to do, assuming no errors. It's the intent of the code and that surely is an important thing for the code to express.
teeray · 14h ago
> ...having only 2 when it comes to error handling does not seem like a big deal. I imagine people will just use their preference...

I foresee endless PR arguments about whether err != nil is the best practice or whatever alternative exists. Back-and-forth based on preference, PRs getting blocked since someone is using the "other" style, etc. Finally the org is tired of all this arguing and demands everyone settle on the one true style (which doesn't exist), and forces everyone to use that style. That is where the "forced to use it" comes from.

From the early days, Go has taken principled stands on matters like this, striving for simplicity and one way to do something. For example, `go fmt` cut through all the tabs vs. space nonsense by edict. "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."

jiehong · 14h ago
The language was first designed without consensus and then released. It was used anyways.

But now a sort of democracy is required for changes. I’m not sure this is necessary.

wrs · 13h ago
The module system shows the Go core team will make unilateral changes if they feel like it. But if I read this correctly, there’s not a consensus even amongst the Go team on what to do here.
throwaway894345 · 12h ago
According to TFA, it's not quite "if they feel like it", it's when there isn't consensus among the community but (1) it's clear that there is a problem and (2) the Go architects can agree on a path forward.
yandrypozo · 14h ago
people that answer surveys don't represent all Go developers, many of us are fine with the error status quo
hathawsh · 14h ago
What I wonder about is the pool of potential Go developers. Is the error handling issue serious enough to stop developers from even considering Go? Go would have been an obviously better choice than most languages 30 years ago, but today there are many more good options.
tuckerman · 14h ago
If you shake things up so much that users who previous dismissed your language are interested, you might also be making a big enough change that your current users look around as well. The pool of prospective new language users is always large but they won’t join a language that is dying because it churned all its existing users and package maintainers.

I say this as someone that gets a very bad taste in my mouth when handling errors in go but use it a fair bit nonetheless.

homebrewer · 14h ago
If you're writing the universe, maybe. There aren't that many competitors when you take the ecosystem into consideration. It is the only reason I tolerate Go where it makes (some) sense — mostly CLI utilities that are too complicated for bash.
arp242 · 13h ago
Every language has the potential to attract new developers if they change/add just this one thing.
throwaway894345 · 12h ago
> Is the error handling issue serious enough to stop developers from even considering Go

If it is, then I suspect those developers are going to have a thousand other non-overlapping reasons not to consider Go. It seems like a colossal waste of time to court these people compared with optimizing Go for the folks who already are using it or who reasonably might actually use it (for example, people who would really like to use Go, but it doesn't support their platform, or it doesn't meet some compliance criteria, or etc).

pixl97 · 12h ago
Ah, voting by survivorship bias.
throwaway894345 · 12h ago
No, this is not an example of survivorship bias. Here's the wiki link for your reference. https://en.wikipedia.org/wiki/Survivorship_bias
pixl97 · 11h ago
No this is an example of survivorship bias.

Let's say Go has such bad error handling that it becomes the number one reason people don't use it.

The people left that do use it will be the ones that don't care about error handling. Hence you're asking the people that don't care versus 90% of the audience you've already lost.

shadowgovt · 14h ago
Python is a bit of a counter-example these days. I think they're in a good place right now, but it's hard to argue they've stuck to the premise of "There should be one-- and preferably only one --obvious way to do it."

- I need to do string interpolation: am I using f-strings or `string.format` or the modulo operator?

- I need to do something in a loop. Well, I can do that. But I could also just do a list or sequence comprehension... Or I could get fancy and combine the two!

And such and so-on, but these are the top examples.

Changing these things strictly adds cognitive load because you will eventually encounter libraries that use the other pattern if you're familiar with the one pattern. And at the limit of what this can do to a language, you get C++, where the spec exceeds the length of the Bible and many bits of it create undefined behavior when used together.

I think Go's project owners are very justifiably paranoid about the consequences of opening the Pandora's box on new features, even if it means users have to deal with some line-noise in the code.

ignoramous · 14h ago
> I don't really understand this decision.

The decision is disappointing, but understandable.

The blog post attempted to explain it, but it comes down to: A lot of energy has been expended without the community and the core team reaching any form of consensus. The current error handling mechanism has entrenched itself as idiomatic for a very long time now. And since the promising ones among the various proposals involve language changes, the core team, which is stretched already, isn't willing to commit to it at this time, especially given the impact.

nimish · 14h ago
This paragraph alone is fundamentally better than the page or so of text in the blog post.

I'm not sure what it is about the style of technical writing I've seen lately but just directly getting to the point versus trying to obfuscate the thesis on a potentially controversial topic is increasingly rare

Pxtl · 12h ago
I've never used Go but having used other languages with exception-based error handling, I get why Go decided to go in a different direction... but reading this over...

Okay, so surely some syntactic sugar could make it more pleasant than the

  if (err != nil) {
    return nil, err
  }
repeated boilerplate. Like, if that return is a tagged union you could do some kind of pattern matching?

... oh, Go doesn't have sum-types. Or pattern matching.

Could you at least do some kind of generic error handler so I can call

  y := Handle(MyFuncThatCanReturnError(x))
?

... Okay, GoLang does not have tuples. Multiple returns must be handled separately.

Okay could I write some kind of higher-order function that handles it in a generic way? Like

  y := InvokeWithErrorHandler(MyFuncThatCanReturnError, x)
?

No? That's not an option either?

... why do you all do this to yourselves?

tail_exchange · 12h ago
This doesn't actually makes the process simpler.

Error handling in Go is not just writing "if err != nil { return nil, err }" for every line. You are supposed to enrich the error to add more context to it. For example:

    result, err := addTwoNumbers(a, b)
    if err != nil {
      return fmt.Errorf("addTwoNumbers(%d, %d) = %v", a, b, err)
    }
This way you can enrich the error message and say what was passed to the function. If you try to abstract this logic with a "Handle" function, you'll just create a mess. You'll save yourself the time of writing an IF statement, but you'll need a bunch of arguments that will just make it harder to use.

Not to mention, those helper functions don't account for cases where you don't just want to bubble up an error. What if you want to do more things, like log, emit metrics, clean up resources, and so on? How do you deal with that with the "Handle()" function?

Pxtl · 8h ago
Obviously I'm being terse for argument.

You can easily imagine

  InvokeWithErrorLogger(fn, fnparam, log)
or

  InvokeWithErrorAnnotator(fn, fnparam, annotatorFn)
Or any other common error-handling pattern.
tail_exchange · 5h ago
Perhaps something like this?

    result := InvokeWithErrorLogger(
        func (err error) { // Error handler
            incrementMetric("foo")
            log.Error("bar")
        },
        addTwoNumbers, a, b,
    )

But the problem is that this approach is not better than just writing this, which doesn't need any new fancy addition to the language:

    result, err := addTwoNumbers(a, b)
    if err != nil {
        incrementMetric("foo")
        log.Error("bar")
        return fmt.Errorf("addTwoNumbers(%d, %d) = %v", a, b, err)
    }
Hence why all the proposals ended up dying with the lack of traction.
HippoBaro · 14h ago
Just to add my two cents—I’ve been writing Go professionally for about 10 years, and neither I nor any of my colleagues have had real issues with how Go handles errors.

Newcomers often push back on this aspect of the language (among other things), but in my experience, that usually fades as they get more familiar with Go’s philosophy and design choices.

As for the Go team’s decision process, I think it’s a good thing that the lack of consensus over a long period and many attempts can prompt them to formally define a position.

ummonk · 14h ago
Yeah once you've been using it long enough for the Stockholm syndrome to set in, you come to terms with the hostile parts of the language.
janderland · 13h ago
I suspect a lot of us don’t have strong feelings either way and don’t find the verbosity “hostile”. No need for Stockholm syndrome if you don’t feel like a prisoner.

Of course you may have been joking, in which case “haha”. xD

Mawr · 7h ago
If you say so. For me it's always been the opposite - I'm excited at the start about all the cool features, then slowly get disillusioned because of the warts.
dangoodmanUT · 14h ago
This, it’s always the new people complaining about error handling.

I have many things to complain about for other languages that I’m sure are top-tier complaints too

tines · 14h ago
I appreciate the argument that things can often be difficult for noobs but actually fine or better than alternatives once you get used to them.

But on the other hand, people who are "used to the way things are" are often the worst people to evaluate whether changes are beneficial. It seems like the new people are the ones that should be listened to most carefully.

I'm not saying the Go team was wrong in this decision, just that your heuristic isn't necessarily a good one.

jchw · 13h ago
This logic mostly only makes sense if your goal is primarily to grow the audience and widen the appeal, though. I think at this stage in the Go programming language's lifespan, that is no longer the goal. If anything, Go has probably started to saturate its own sweet spot in some ways and a lot of complaints reveal a difference in values more than they do opportunity for improvement.

To me, it makes sense for the Go team to focus on improving Go for the vast majority of its users over the opinions of people who don't like it that much in the first place. There's millions of lines of code written in Go and those are going to have to be maintained for many years. Of utmost priority in my mind is making Go code more correct (i.e. By adding tools that can make code more correct-by-construction or eliminate classes of errors. I didn't say concurrency safety, but... some form of correctness checking for code involving mutexes would be really nice, something like gVisor checklocks but better.)

And on that note, if I could pick something to prioritize to add to Go, it would probably be sum types with pattern matching. I don't think it is extremely likely that we will see those, since it's a massive language change that isn't exactly easy to reconcile with what's already here. (e.g. a `Result` type would naturally emerge from the existence of sum types. Maybe that's an improvement, but boy that is a non-trivial change.)

jiehong · 14h ago
It’s fun, because when a newcomer joins a team, people tend to remind them that their bison is fresh and they might be seeing pain we got accustomed to. That’s usually said in a positive manner.
tialaramex · 13h ago
I'm intrigued as to whether "bison" here is a metaphor (for what?) or a cupertino (an error introduced by auto-correct or predictive text)
abtinf · 14h ago
I have a similar level of experience with Go, and I would go so far as to say it is in fact one of the best features of the language.

I wouldn’t be surprised that when the pro-exception-handling crowd eventually wins, it will lead to hard forks and severe fragmentation of the entire ecosystem.

jchw · 13h ago
To be honest, I really don't believe that will happen in the future. All of the proposals pretty much just add syntactical sugar, and even those have failed to gain consensus.
zarzavat · 13h ago
That's just survivorship bias isn't it? The newcomers who find Go's design and/or leadership obnoxious get a job that doesn't involve doing something that they dislike.
arp242 · 13h ago
That's okay. Not everyone needs to like Go. Pleasing every programmer on the planet is an unreasonable thing to ask for. It's also impossible because some preferences conflict.
zarzavat · 11h ago
After over a decade of people bringing up the issue in almost every single thread about Go, it's time to give the language what it deserves: no more constructive feedback, snarky dismissals only.
arp242 · 8h ago
Not infrequently by people who are not even Go programmers. And/or the same people who hijack every other Go thread to rant about how much they hate Go.

In a quick search, you seem to be one of them: https://news.ycombinator.com/item?id=41136266 https://news.ycombinator.com/item?id=40855396

You don't see me going around $languages_I_dislike threads slagging off the language, much less demanding features. Not saying anything is an option you know.

kubb · 14h ago
TLDR: we didn't fix it for such a long time that now it's too late to fix it. We won't be fixing it, thanks!

Edit: looking at the results of their H1 2024 survey, they're making a hard turn into AI, and most likely will be developing AI libraries to close the gap with Python.

Don't expect language features.

henry700 · 14h ago
>For a while, the lack of generics surpassed complaints about error handling, but now that Go supports generics, error handling is back on top

>The Go team takes community feedback seriously

It feels like reading satire, but it's real.

danenania · 14h ago
They clearly are wrestling with these issues, which to me seems like taking the feedback seriously. Taking feedback seriously doesn’t imply you have to say yes to every request. That just gives you a design-by-committee junk drawer language. We already have enough of those, so personally I’m glad the Go team sticks to its guns and says no most of the time.
ummonk · 13h ago
How is Go not a design-by-committee language? They don't have a single lead language developer or benevolent dictator, and as this blog demonstrates, they're very much driven by consensus.
danenania · 13h ago
True, but they’re very picky as committees go. But yeah, maybe not the best use of that expression…
righthand · 15h ago
Hopefully this is the final nail in the coffin for all the “but I have to handle my errors” whining.
aystatic · 15h ago
I don't know that there's whining about "having to handle errors" in principle, it's pretty clearly a complaint with the syntax and semantics of doing so

Some languages even make omitting error handling impossible! (e.g. Result sum types). None have anywhere near the amount of "whining" Go seems to attract

codr7 · 15h ago
I just wish the could stabilize the compiler before piling on more features.

I regularly run into internal compiler errors these days for pretty normal looking code.

It's getting to the point where I'm reluctant to invest more time in the language right now.

UPDATE: See comment below for full error message and a link to the code.

Thaxll · 15h ago
I've never seen a single internal compiler issue in 10 years of working in Go.

What error are you talking about?

kbolino · 14h ago
Not OP, but I found a compiler bug in the initial release of 1.18, though it was also quickly fixed: https://github.com/golang/go/issues/51733
codr7 · 14h ago
Me neither until the latest version.

Internal compiler error, type2 something, unknown line number.

I'll have to look it up.

Downvotes for reporting an error, very classy HN.

UPDATE: See comment below for full error message and a link to the code.

johnfn · 15h ago
In that case, you might enjoy this part of the article:

> For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling

shivamacharya · 15h ago
If you do stuff the language doesn’t support, it’s not the languages problem.
codr7 · 14h ago
I did nothing weird, played around a bit with the new iterators.

Internal compiler errors are very much the implementation's problem.

UPDATE: See comment below for full error message and a link to the code.

shivamacharya · 14h ago
I only say this because I have yet to encounter a single internal compiler error in years of writing Go professionally. I can’t conceive of the kind of code one must be writing for internal compiler errors to be a repeated issue.
codr7 · 13h ago
So here's the complete error message:

<unknown line number>: internal compiler error: unexpected types2.Invalid

Please file a bug report including a short program that triggers the error. https://go.dev/issue/new

And here's the code that triggers it:

https://github.com/codr7/shi-go/blob/main/src/shi/call.go

The code is never referenced in the project, but running make in the project root with this file in it triggers the error. Remove the file and the error disappears.

Happy now?

kiitos · 10h ago
Paste exactly what commands you run and exactly their output, including exactly the rev of the repo you're running them in.

> Remove the file and the error disappears

Remove the file and the code no longer compiles, because the file contains definitions that are used by other code in the package. If removing that file doesn't break your build, something is wrong with your build!

Your Makefile seems to be calling `go test src/tests/*` which is invalid syntax, I suspect that's just one of many similar kinds of mistakes, and likely indicative of a misunderstanding of the language tooling...

> https://github.com/codr7/shi-go/blob/main/src/shi/call.go

This code is buggy from tip to tail, my goodness! Starting with no `gofmt` formatting, everything in https://github.com/codr7/shi-go/blob/main/src/shi/vm.go, invalid assumptions in everything that embeds a Deque, no meaningful tests, misuse of globals, the list goes on and on... ! It seems like you're programming against a language spec that you've invented yourself, maybe influenced by Go, but certainly not Go as defined!

catlifeonmars · 6h ago
I’m somewhat surprised this was a topic that was deemed worthy of a blog post. It seems quite out of character with other blog posts on the Go blog which are usually about introducing a new feature. Sure, Go’s relatively verbose error handling has long been a source of controversy, but was it worth publishing a blog post just to say, effectively: “please shut up”?