Skip to content(if available)orjump to list(if available)

(On | No) Syntactic Support for Error Handling

threemux

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

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

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

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

What makes you think Rust’s compile times are related to its type system?

Yoric

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

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

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

karmakaze

At least Java supports covariance and contravariance where Go only supports invariant generics.

throwaway2037

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

platinumrad

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

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.

ablob

Afaik the F# supports do notation through computation expressions.

evntdrvn

hahaha :D

yusina

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

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

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

Yoric

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

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

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

Indeed, I can testify that `?` was very much designed with the do-notation in mind.

9rx

> and yet nowhere on that page is the Haskell-solution mentioned

What do you mean? Much of the discussion around errors from above link is clearly based on the ideas of Haskell/monads. Did you foolishly search for "monad" and call it a day without actually reading it in full to reach this conclusion?

In fact, I would even suggest that the general consensus found there is that a monadic-like solution is the way forward, but it remains unclear how to make that make sense in Go without changing just about everything else about the language to go along with it. Thus the standstill we're at now.

mark38848

Why not change everything along with it? It can only get better

anentropic

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

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

Merovius

> 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 […]

This is a bold statement for something so subjective. I'll note that the proposal to leave the status quo as-is is probably one of the most favorably voted Go proposals of all time: https://github.com/golang/go/issues/32825

Go language design is not a popularity contest or democracy (if nothing else because it is not clear who would get a vote). But you won't find any other proposal with thousands of emoji votes, 90% of which are in favor.

I get the criticism and I agree with it to a degree. But boldly stating that criticism as objective and universal is uninformed.

ok_dad

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

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

I don't see how Try (the ? operator) is hidden control flow. It's terse, but it's not hidden.

munificent

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

(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…

skywhopper

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.

ivanbakel

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

This is a case of massive selection bias. How do you know that Go’s error problem isn’t so great that it drives away all of these programmers? It certainly made me not ever want to reach for Go again after using it for one project.

hackingonempty

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

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

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

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

throwawaymaths

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

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

mamcx

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

that is weird that they call it a Wiki when it is not a wiki any more - you have to submit changes for approval

arccy

unfortunately too much spam but the review process is much lighter

pie_flavor

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

"despite multiple complete perfect proposals on how to do it"

There is no such a thing.

arccy

and this is how rust gains its reputation as an ugly to read language with inconsistent syntax: design by committee

codedokode

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

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

you don't Go is explicit about things.

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

jeremyjh

Whereas Go has taken the approach of designing everything "later".

mseepgood

It's good to have languages with different approaches to design and and with different design philosophies.

9rx

To be fair, Go, under the watch of Google, deemed it a finished product as far as the language goes. "Later" wasn't expected to happen. It was complete already. Only when the outside community took control of the project did it start considering new features.

olalonde

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

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

Ugly is subjective, but which part of the syntax is inconsistent?

j-krieger

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.

dlisboa

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.

xigoi

Just some examples I thought of:

* Array types have completely different syntax from other generic types

* &mut T has a space between the qualifier and the type, &T doesn’t

* The syntax for anonymous functions is completely different from a function declaration

arccy

.await something that looks like a field access but actually does something else

dcow

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

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

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.

stouset

> Programming languages are designed systems, they need to make sense holistically.

Of all the languages in common use, golang is the one that makes the least sense holistically. Return values are tuples, but there's nothing that lets you operate on them. Enums aren't actually limited to the values you define, so there's no way to ensure your switch cases are exhaustive when one is added in the future. Requiring meaningful zero values means that your error cases return valid, meaningful values that can accidentally be used when they return with an error.

827a

This opinion is clearly far more arguable than you might think.

hu3

> 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

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

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"

9rx

I'm as satisfied with the error handling approach as I am for the email address handling approach, the time of day handling approach, the temperature handling approach, etc.

But that doesn't imply that I am satisfied. I do believe there is a lot of room for improvement. Frankly, I think what we have is quite bad. Framing it as something about errors misses the forest for the trees, though.

How would I respond to your query without misleading the reader?

827a

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

!RemindMe in 25 years.

Did Rust become a clusterfuck like C++?

Is Go as timeless as it was during release?

GoatInGrey

> 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

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

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

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.

bravesoul2

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

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

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
    }

umanwizard

Something slightly more elegant (in my subjective opinion) you could do is write

  if !(err != nil) {

hnlmorg

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.

macintux

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

Just tried this and it appears to be valid in the compiler, formatter and golangci-lint

null

[deleted]

vhcr

Would be nice if code editors colored it differently so it's easier to see.

prerok

return nil

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

9rx

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

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

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?

purpleidea

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

> then you have _that_ "littering" your code, instead of the syntax.

Yes, exactly. The unusual thing _should_ look unusual.

9rx

The unusual case does look unusual. == and != are visually very different.

I suspect the real problem here is that the parent commenter forgot (read: purposefully avoided) to write tests and is blaming the tools to drown his sorrow.

derefr

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

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

Good point. Perhaps it could also be solved in an editor with a collapsed notation like ‘if err … {‘

matthewmueller

I like nil == err for this case

TZubiri

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

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

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

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

> 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

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

Yet the problem still has happened on big projects:

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

pphysch

> 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

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

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.

jamamp

In primarily throwable languages, it's more idiomatic to not include much error handling throughout the stack but rather only at the ends with a throw and a try/catch. Catching errors in the middle is less idiomatic.

Whereas in Go, the error is visible everywhere. As a developer I see its path more easily since it's always there, and so I have a better mind to handle it right there.

Additionally, it's less easy to group errors together. A try/catch with multiple throwable functions catches an error...which function threw it though? If you want to actually handle an error, I'd prefer handling it from a particular function and not guessing which it came from.

Java with type-checked exceptions is nice. I wish Swift did that a bit better.

ignoramous

At this rate, I suspect Go2 is an ideas lab for what's never shipping.

maxwellg

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

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.

pas

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

Even bash has -e :)

bravesoul2

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.

null

[deleted]

evertedsphere

rust-style "sum type" errors are values too

9rx

But, of course, makes the incorrect assumption that <T, E> are dependent variables. The idiomatic Go approach is much more representative of reality. Tradeoffs, as always.

masklinn

You have things the wrong way around.

A developer uses Result because T and E are exclusive. If they’re not, they will use something else. And it will be clear to the caller that they are in a rare oddball case.

The idiomatic Go approach makes no provision for such distinctions at all.

Mond_

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

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

Test coverage as a target metric is stupid.

g7r

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.

null

[deleted]

ncruces

It this really bothers you, convince your team to use courtney and focus on the more relevant error branches: the ones where (1) a novel error value is produced or (2) handled rather than those that simply bubble it up.

https://github.com/dave/courtney

neild

> 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

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

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.

SkepticalWhale

I agree with item #1, but it can be mitigated somewhat with dev tools like errcheck: https://github.com/kisielk/errcheck?tab=readme-ov-file

VirusNewbie

Agreed, 100%.

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

tsimionescu

> 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

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

> 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

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

tsimionescu

I'm not arguing that stack frames are as good as manually written error traces can be - they're clearly not. I am simply amazed that people include "generate error traces" as a form of "handling" an error.

The argument for explicit error values is often something like "it encourages people to actually handle their errors, rather than ignoring them". And on the face of it, this has some merit: we've all seen code that assumes an HTTP request can't fail, and now a small timeout crashes the entire backup procedure or whatever.

But if "handle the error" simply means "decorate it with a trace and return it", then exceptions already do this, then you're really admitting that there is no fundamental difference from a exception, because this is exactly what exceptions do, all on their own. Sure, they produce less useful traces, but that's usually a tiny difference. After all, the argument wasn't "you'll get better stack traces than exceptions give you", it was "people will be more careful to handle errors".

This is also relevant, because if the goal is to get better error traces, that can also be done with exceptions, with just some small improvements to syntax and semantics (e.g. add syntax for decorating a call site with user supplied context that will get included in any exception bubbled from it; add support in an exception to only print non-library stack frames, add support in the language to declare certain variables as "important" and have them auto-included in stack traces - many ideas).

teeray

> there is no fundamental difference from a exception

Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with. Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto. Can this method fail? Who knows! What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it.

jitl

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.

JamesSwift

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

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.

masklinn

> 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

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.

stouset

> 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

> 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

> 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

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.

tayo42

You need to implement from for every type of error then? That seems pretty tedious also.

sa46

> 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

9rx

> 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

> 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

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

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

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.

9rx

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.

mseepgood

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

abtinf

> 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

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

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

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?

umanwizard

Oh no, you are making the classic mistake of assuming Go’s designers did something that would make sense, rather than picking the most insane possible design in a given situation.

kbolino

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

Thanks, I hate it.

JamesSwift

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

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

d3ckard

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

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.

Ferret7446

We already have languages that ship features. Go is a lone lighthouse of stability in a sea of fancy languages. I'll play with your fancy languages, but I build my own projects that I actually use in Go because I can trust that it will keep working for a long time and if/when I need to go back to it to fix something in a couple of years I don't need to re-learn a bunch of crap that might have seeped through dependencies or the stdlib.

MeetingsBrowser

> 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

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

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

You can at least in Go do this:

r, err := f()

r := f()

_, err := f()

knutzui

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

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

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

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)

juped

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

null

[deleted]

reader_1000

> 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

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

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.

_benton

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!

AnimalMuppet

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

Not a compiler check, but staticcheck is widely used:

    % staticcheck test.go
    test.go:7:2: this value of err is never used (SA4006)

dmitshur

Yeah, as one data point, https://staticcheck.dev/docs/checks/#SA4006 has existed since 2017.

masklinn

There very much is not. There is a compiler error you can’t disable if a variable is completely unused and that is it.

hackingonempty

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

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

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?

9rx

> they could have something like this:

Could in some purely theoretical way, but this is pretty much exactly the same, minor syntax differences aside, as virtually every other failed proposal that has been made around this. It would be useless in practice.

In the real world it would have to look more like this...

    func printSum(a, b string) result[error, unit] {
        return for {
            x <- strconv.Atoi(a) else (err error) {
                details := collectDetails(a, b)
                stop firstConvError{err, details}
            }
            y <- strconv.Atoi(b) else (err error) {
                stop secondConvError{err}
            }
        } yield fmt.Println("result:", x + y)
    }
or maybe 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)
    }

    handle(printSum, "x", func(vars map[string]any, err error) {
        details := collectDetails(vars["a"].(string), vars["b"].(string))
        return firstConvError{err, details}
    }

    handle(printSum, "y", func(vars map[string]any, err error) {
        return secondConvError{err}
    }
...because in that real world nobody just blindly returns errors like your contrived example shows[1]. There are a myriad of obvious (and many not so obvious) problems with that.

Once you start dealing with the realities of the real world, it is not clear how your approach is any better. It is pretty much exactly the same as what we have now. In fact, it is arguably worse as the different syntax doesn't add anything except unnecessary complexity and confusion. Which is largely the same reason why all those aforementioned proposals failed. Like the linked article states, syntax is not the problem. The problem is that nobody to date knows how to solve it conceptually without completely changing the fundamentals of the language or just doing what you did, which is pointless.

And there is another obvious problem with your code: result[error, unit], while perfectly fitting in other languages designed with that idea in mind, is logically incorrect in the context of Go. They are not dependent variables. It doesn't make sense to make them dependent. For it to make sense, you would, as before, have to completely change the fundamentals of the language. And at that point you have a band new language, making any discussion about Go moot. That said, I think the rest of your idea could be reasonably grafted onto the (T, error) idiom just as easily. However, it still fails on other problems, so...

[1] Which, I will add, is not just speculation. The Go team actually collected data about this as part of their due diligence. One of the earlier proposals was on the cusp of acceptance, but in the end it wasn't clear who – outside of contrived HN comments – would ever use it due to the limitations spoken of above, thus it ultimately was rejected on that basis.