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.
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.
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...
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!
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
}
> 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?
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?
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.
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.)
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.
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.
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.
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?
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?
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.
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.
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
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:
- 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?
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.
I've been programming in PHP since 2002 and Go since 2011. At work I work primarily with two large PHP codebases, 1 about ~half a million lines of code, and the other about ~3 million lines of code. I'm porting PHP code to Go and writing some new code, like cron jobs, in Go. There's about 55,000 lines of Go code at this point.
What I have learned in this time is: Go is a very good, very high quality, very well designed language. PHP is a terrible, terrible, terrible language -- at least if your mission is creating highly reliable software. If your goal is rapid prototyping of the visual appearance of a webpage, PHP is good for that -- that's what it was originally invented for, back in the 1990s, and it's still the best language for that specific use case.
In PHP, if you are looking at a line of code, it's impossible to tell what happens if an error occurs. There's an error_reporting level that can be changed anywhere else in the codebase at any time. So an error might be thrown in the user's face, or not -- depending on the error_reporting level and what the error is. This can also interact with output buffering. (I guess if the "@" sign is present, at least then you know output of an error message is suppressed.) There's a error_handler function that could be set. Oh but it gets worse! Even if you have the entire codebase and can scour every line of code, and trace every possible execution path, you still can't tell what will happen if an error occurs at the line of code you are looking at -- because there are php.ini settings that come into play!
This is without even mentioning the try/catch exception handling system, which PHP also has, and that you think is such a good thing. So if an error happens on the line you are looking at, execution may switch to somewhere above you in the call stack. But (at least in theory) if you have the complete source code, you can figure out all the places where that might be.
This is an absolute nightmare if you care about reliability. The worst thing you could possibly have in a programming language if you care about reliability is to be unable to tell what happens when an error occurs and reason about the program's behavior.
Contrast this with Go. In Go, I can know what happens if an error occurs because it's rightthere. There's an "if err != nil {..." block that tells me exactly what happens.
By porting code to Go, I've been able to increase the reliability tremendously. The thing about go, is that using Go doesn't automatically give you reliability -- you have to learn the techniques and tools in Go that give you reliability. But once you do that, the quality of software you can produce is vastly higher.
I've spent the last decade or so learning this. Learning how to structure the code to make maximum use of the compiler's static type checking to catch errors. Making use of the static analysis tools that catch errors. Using a combination of errcheck and staticcheck (mentioned elsewhere in this discussion), you can guarantee every err return value is actually checked and handled. So the oft-cited language flaw of Go allowing you to forget to handle errors becomes a non-issue. Go has built-in support for unit tests and the excellent polymorphism system makes tests easier to make than in PHP. And, getting back to the topic at hand, in Go, I created my own system for handling the err values that get returned so junk errors created by hackers trying to find security vulnerabilities on our server get filtered out, while actual errors that I need to fix get logged and brought swiftly to my attention. Can you do something similar in PHP? In theory, perhaps, but we are to afraid of breaking things to try to massively refactor our ~2 million lines of PHP code in order to do that. So we have various log files and database entries that get clogged with errors, and sometimes we notice errors that we need to fix, but usually we don't. U...
Global settings are easy to check and verify and modern frameworks handle it for you, because there's all these knobs. (I agree it's waaay too many, but that's because there's a runtime and on top of that there's a process manager, and on top of that there's nginx/apache or other reverse proxy.)
Sure, someone can write a Go library to wrap every low-level function to make sure there's some error handling, maybe with closures and generics it would be quite okay.
In Go if you don't see the error handled you know it's not handled anywhere else. Great? Well, sure .. um, maybe? After all if you want to handle it you need to add error handling there. Consequently your code now is 3x as many lines and ~66% of it is returning errors upward. It's the new Assembly.
(I don't think try-catch is good, I think that PHP's error handling is better despite try-catch.)
Porting to Go (for reliability or otherwise), why? There are other languages out there! Especially if you spent the last decade learning about compile-time checks.
I know that k8s (and tons of now-critical software) is written in Go, and it's not a pretty sight -- and instead of having better abstractions there's NASA-cargo-culting[0]. Linux is written in C. It does not make C a great choice for many reasons. (Go is definitely a better choice than C when it comes to memory safety for example, but I prefer Scala or Rust.)
Facebook added their own typing to PHP (and tellingly called it Hack, of course).
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.
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.
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.
No. You have not considered what is going on around you.
> A developer uses Result because T and E are exclusive.
A programming language may be designed around that premise, but that is not the case in the real world (faults in the real world are never binary), and it is certainly not aligned with the design of Go.
> The idiomatic Go approach makes no provision for such distinctions at all.
Naturally. It logically can't because T and E are both considered to be always valid per the fundamentals of the language. The idioms emerged as an extension of those core principles. As before, other languages may take different view, but Go isn't those languages.
That's what makes this changing error handling business so hard. If Go were a completely different language then it would be easy to adopt something like monads, but for the Go we have things like that just don't logically fit and it is not yet clear what does.
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?
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.
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.
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.
> 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.
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).
> 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.
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.
>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.
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.
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.
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
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.
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.
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.
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.
> 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).
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.
I don’t think you understood my comment. My argument was not “we should only care about Go users”, it was that we should not prioritize the opinions of people who may not even become users even if error handling is changed over the opinions of people who are very likely to use Go or those who already do.
Specifically, if Go’s error handling poses a constitutional objection for you, it’s probably just one item in a long list of things that prevent you from using the language. Changing everything to pacify you will take a long time and likely involve many breaking changes, and the end result is likely to be something that does not appeal to Go’s users or even many of the people who shared your objection about error handling but not all of your other objections.
> ...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."
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;"
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.
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.
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.
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.
In the module system case, there was a lot of consensus in the community, including widely-used and pretty mature tooling, and the core team surprised the community by implementing their own approach unilaterally. That predated the current rules of engagement. But in a similar situation today where the core team felt the community’s consensus was wrong, I wouldn’t be too surprised if that happened again.
> there was a lot of consensus in the community, including widely-used and pretty mature tooling
At the time the community was pretty fragmented between vanilla GOPATH, vendoring, godep, and one or two others that are escaping my memory. I don't think that meets my criteria for "a lot of consensus".
Probably a better example would be the type alias stuff that was introduced pretty explicitly to support Google's use case without much consultation from the wider community. That caused some kerfuffle as well; however, that also caused the maintainers to change their stance and lean into the community a lot more.
But yes, both of these examples predate any formal "rules of engagement" with the community and things have generally been better since. Moreover, these are the only two examples I can think of where the Go team pushed through some controversial, significant change. The Go team is extremely conservative (which is something I value, for the record), and far more likely to make no change at all even when there is a lot of enthusiasm for some particular change.
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.
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".
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
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.
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).
Stack traces are full of noise by comparison and don't have context added by the programmer at each frame. For me, Go error chains are much easier to work with. I can see the entire flow of the error at a glance, and can zero in on the relevant call in seconds with a single codebase search.
Stack traces are not so long that you can't find the information you need in them, and actually just like go in some languages you can elect to add context to your stack trace (e.g. in python by raising an error from another error).
My experience in go was opposite of yours. The original devs (who were long gone) provided no information at all at the error site and I felt lucky even to find the place in the code that produced the error. Unfortunately the "force you to handle errors" idea, while well intentioned, doesn't "force you to provide useful error handling information", making it worse than stack traces by default.
> 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.
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.
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.)
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"
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?
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.
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.
It's also worth considering that many editors have default snippets that transform something.await into (await something) for languages with prefix await, so it kind of makes sense to cut out the middle man and just make that the syntax to me.
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.
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.
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.
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.
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.
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.
It would need to be &u8, as without a reference, you won't have any captured lifetimes, and therefore it wouldn't serve as an example for the capturing lifetime feature. &u8 is also a mostly pointless type in reality.
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.
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.
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
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
It should be mentioned that within the python community using __ for extra private is widely seen as a misfeature that shouldn't've been added and shouldn't be used.
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.
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.
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.
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.
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.
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.
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.
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.
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?
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.)
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.
Exactly; `return fmt.Errorf("annotation: %w", err)` is just a log-friendlier version of `return err`. The original wrapped error is meant for runtime inspection.
I think we are saying the same thing: it is important metadata which is meant for human consumption (as opposed to the wrapped sentinel error) -- hence "log-friendly".
> `return fmt.Errorf("annotation: %w", err)` is just a log-friendlier version of `return err`
This claim isn't true. The annotation-wrapping isn't just about making the returned error 'log-friendlier', it's annotating the error with context-specific information that's useful beyond "log" contexts. More concretely, that kind of annotation is critical if you expect to be able to introspect runtime errors in any way at all -- it's not a "log-friendlier" version of `return err`, it's an important and necessary change vs. `return err` which is in no way perfunctory or whatever.
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.
> 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.
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.
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.
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.
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'
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?
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.
> 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.
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.
> 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).
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
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)
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.
> 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.
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).
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.
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.
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.
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.
> 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.
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.
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.
The idea doesn't even make any sense. How could you even begin to handle errors in a meaningful way if you were unable to discern what type the error is?
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
}
> 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)?;
// ...
}
`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")?;
// ...
}
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.
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?
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)
... 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.
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.
> 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.
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.
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.
Yeah, people complain about `if err != nil`, but the real issues are tricky rules around variable shadowing, lack of nil safety and immutability, cgo, defer semantics, etc
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.
- 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.
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.
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.
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.
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?
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.
> 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...
> That does not give reason to only solve for a narrow case when you can just as well solve for all cases.
...
This clearly can't be solved "just as well" because nobody can figure out how to do it. The second half of your comment alludes to this, but a lot of what makes this hard to solve are pretty inherent to the design of the language, and at this point, there's a pretty large body of empirical evidence showing that there's not going to be a solution that elegantly solves the issue for every possible theoretical case. Even if someone did manage to come up with it, they're literally saying that they wouldn't entertain a proposal for it at this point! I don't understand how you can come away from this thinking it's realistic that this would get solved in some general way.
> 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.
That's exactly the argument for solving this for what you're calling a "narrow" case. Providing syntax just for (T, E) that uses the zero value for T when short-circuiting to return E would improve the situation from a human perspective, even if it meant that to utilize it for more than two return values you need to define a struct for one or both of T or E. The only objections to it that you're raising are entirely from the "computer" perspective of needing to solve the problem in a general fashion, which is not something that needs to be done in order to alleviate the issues for humans.
> This clearly can't be solved "just as well" because nobody can figure out how to do it.
Fine, but then that means there is no other solution for Go unless you completely change the entire fundamental underpinnings of the language. But, again, if you're going to completely change the language, what's the point? Just use a different language that already has the semantics you seek. There are literally hundreds of them to choose from already.
> That's exactly the argument for solving this for what you're calling a "narrow" case.
Go has, and has had since day one, Java-style exception handlers. While it understandably has all the same tradeoffs as Java exception handling, if you simply need to push a value up the stack, it is there to use. Even the standard library does it when appropriate (e.g. encoding/json). The narrow error case is already covered well enough - at least as well as most other popular languages that have equally settled on Java-style exception handling.
Let me be clear: It is the general case across all types that is sucky. Errors, while revealing, are not the real problem and are merely a distraction.
I assume with "Java-style exception handlers" you're referring to panics? If this were sufficient, then people would be using it more for error handling. I'd argue that a large part of why people don't write code using them as error handling more often is because of the syntax, and that's ultimately what this whole discussion is for me. This is probably another area where we just fundamentally disagree, because I don't really consider there to be a necessity to fully solve the "real problem" instead of providing something smaller that alleviates specific minor warts.
What's strange to me is that the main reason this seems like the best path forward to me is because it doesn't require large fundamental changes to the language, and I'm skeptical that there's any fix to the underlying issues that you're concerned about that wouldn't require those sorts of changes, but somehow your objections seem to mostly be on the grounds that you also don't want those types of changes. To me, the insistence the entire real problem needs to be solved for anything to be worth doing is in practice incompatible with the requirement not to change the language fundamentally, so the question becomes whether its worth considering changes that don't fully solve what you consider to be the real issue. I think that's where the disconnect is; I'm not really trying to argue for a solution to the problem you're concerned about because I don't consider it to be realistic that it will ever get solved, so I'm arguing that making smaller changes to reduce the impact of the problem without solving it fully would be worthwhile compared only to the status quo rather than some ideal solution that I don't think exists. I'm not trying to say that it's an absolute certainty that there's no way to fix the issues you're concerned about without fundamentally changing the language, and I'd be just as happy as you if I turn out to be wrong! It doesn't really feel like you're rebutting the actual suggestion I'm making though because you're interpreting my claim that there isn't any way to solve the problem generally without changing the language fundamentally as my advocating for those fundamental changes, which is not what I actually think, and from my perspective isn't what I've been saying at all.
> I'd argue that a large part of why people don't write code using them as error handling more often is because of the syntax
I don't see meaningful difference in the syntax as compared to other languages with a similar feature: https://go.dev/play/p/RrO1OrzIPNe How deep are we really going to split hairs here?
If it were commonly used you could clean it up a little, like how the somewhat recently added iterators feature cleaned up what people were already doing with iteration, but in this case since it is so rarely used in the first place, why bother? "If you build it, they will come" is Hollywood fantasy. Unlike this, the use of iterators was already prevalent before the iterators feature was added.
Let's be honest: If it were useful, people would already put up with the above being slightly less than perfect. People will actually put up with a lot of shit when something is useful at its core! But that they are doing this almost never is quite telling.
> What's strange to me is that the main reason this seems like the best path forward to me is because it doesn't require large fundamental changes to the language
Or maybe no changes at all. Would using the above really be so bad from a syntactical point of view? The much bigger problem, and why pretty much all modern languages have moved to returning errors as the defacto solution, is that it exhibits all the same fundamental problems as errors under Java exception handling. That is something syntax cannot fix.
And, well, for the exceptional (pun intended?) cases where Java-style exception handling really its the best option to suit your circumstances: It's there to use already!
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.
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.
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.
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.
> [I] 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.
Clearly not different enough.
Tests are just one tool among many that we use to build and evaluate mental models of behaviour. It's equally possible that the parent commenter noticed unusual behaviour _via_ their tests, and took "ages to debug" precisely _because_ they were misreading the code while trying to understand _why_ the tests were failing. A hypothetical syntax highlighter that flagged up to them "hey, you're doing something unusual here - is that intended?" would have helped them in debugging _alongside_ tests.
If you take the word as gospel, but why should we? It is hard to believe. As shocking as it may be, not everything you read on the internet is true.
Either way, the fact of the matter is that discussion about code is silly without code. Since I have no knowledge of the actual code in question, which has suspiciously been kept a secret for some reason, I'll open the bidding with this: https://go.dev/play/p/xEnGTmJ_57g — From the output alone, you don't think you'd be able to gain a pretty good idea of what the problem might be?
Feel free to update the code with something more real-worldy if you think the contrivedness of it masks what you are trying to talk about. We had to start somewhere.
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".
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.
“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.
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.
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.
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.
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.
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.
It's a pale shadow of an actual Option/Result type, though, and the way most community and professional Elixir code uses ok-tuples is hardly what I would call rigorous. The with special form leads to sloppy pass-the-buck design IME, and nothing in the language design or Credo ruleset is likely to steer anyone to do better.
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:
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.
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?
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.
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.
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.
It does. Hell, Go also has a goto statement as well, although obviously that's unstructured control flow.
A more refined version of what I originally said would say "conditional branch" instead of "branch", but I'll admit that my original message should have been worded more carefully. I think people understood it, but taken literally it's not a strong argument.
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...
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.)
> but it wasn't a mistake to not have exceptions in Go, it was intentional.
It does have them, though, and always has. Use is even found in the standard library (e.g. encoding/json). They are just not commonly used for this because of the inherit problems with using them in this way as you have already mentioned. But you can. It is a perfectly valid approach where the tradeoffs are acceptable.
But, who knows what the future holds? Ruby in the early days also held the same preference for error values over exceptions... Until Ruby on Rails came along and shifted the prevailing attitude. Perhaps Go will someday have its "Ruby on Rails" moment too.
Disagree. We could argue what counts as "exceptions", the jargon goes places (e.g. CPU exceptions are nothing to do with "exception handling" for example.) I'd argue that in the modern day programming language exception handling is the type where you have structured control flow dedicated to just the exception handling. Go has stack unwinding with panic and recover, but those are just normal functions, there's no try, no catch, and no throw, and no equivalent to any of those. C also has setjmp/longjmp which can be used in similar ways, but I wouldn't call that exception handling either.
But I think we'll have to agree to disagree on that one, since there's little to be gained from a long debate about what jargon either does or should subjectively mean. Just trying to explain where I'm coming from.
What is there to debate? An exception, by every definition I have ever encountered, is a data structure that contains runtime information (e.g. a stack trace) to stand in for a compiler error where the compiler was not sufficiently capable of determining the fault at compile time. It couldn't possibly mean anything else in reason.
Of course, we're really talking about "exception handlers", not "exceptions".
> there's no try, no catch, and no throw, and no equivalent to any of those.
There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
Think about it. Go could easily provide syntax sugar that replaces `try { throw() } catch (err) {}` with `try(func() { throw() }).catch(func(err) {})`. That would truly satisfy your requirements in every way. But what, specially, in that simple search and replace operation says "exceptions" (meaning exception handlers)?
> C also has setjmp/longjmp which can be used in similar ways, but I wouldn't call that exception handling either.
Agreed. You could conceivably craft your own exceptions to carry through the use of setjmp/longjmp, but that wouldn't be a language feature. However, Go does have an exception structure as a built-in.
The Wikipedia article about Exception handling[1] does a better job discussing the history and background IMO. Also, obviously when we say "exceptions" in a programming language, we're definitely talking about "exception handling", the word is omitted because it's obvious on context. I'd argue that one's on you if you thought otherwise. (If we're just talking about an "exception", well the Go error object is an "exception", but it's pretty obvious you're not merely talking about that.)
True to my word, I won't argue over the definition itself.
> There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do. For example, Go doesn't have pattern matching; implementing an equivalent feature with closures does not make this any less true.
> The Wikipedia article about Exception handling[1]
What's that old adage? I think it goes something like "The wiki is always accurate—except when it’s about something you know personally." If you don't enough about the topic to discuss it yourself, what are you doing here?
> Also, obviously when we say "exceptions" in a programming language, we're definitely talking about "exception handling"
Not necessarily. Often it is important to discuss the data structure and not the control flow. Strictly, "exception" refers to either the broad concept of exceptional circumstances (i.e. programmer error) or the data structure to represent it. "Exception" being short for "exception handling" where context is clear is fine, but be sure context is clear if you want to go down that road – unless you like confusing others, I suppose.
> well the Go error object is an "exception"
You mean the error interface? That's not an exception. It's just a plain old interface; literally `type error interface { Error() string }`. In fact, the only reason it gained special keyword status is because it being part of the standard library, where it was originally defined in early versions, caused cyclical import headaches. If Go supported circular imports, it would be a standard library definition instead of a keyword today.
The underlying data structure produced when calling panic is an exception, though. It carries the typical payload you'd expect in an exception, like the stack trace.
Of course, errors and exceptions are conceptually very different. Errors are for things that happen in the environment – invalid input, network down, hard drive crash, etc. Exceptions are for programmer mistakes – faults that could have theoretically been detected at compile time if you had a sufficiently advanced compiler. Obviously you can overload exceptions to carry unexceptional information (as you can overload errors to carry exceptional information), and a pragmatist will from time to time, but that's not the intent for such a feature[1].
> To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do.
Aside from the fact that there is actually an exception involved. Again, while you might be able to hand roll your own exception data structure in C, it does not provide it for you like Go does. If setjmp/longjmp were paired with an exception data structure of the box, it would reasonably considered exceptions, naturally.
However, the second bit was the real meat of that. A tiny bit of search and replace and you have a system that is effectively indistinguishable from exception handling in languages like Java, Javascript, etc. You haven't explained what about that search and replace, that does not introduce any other new language features or introduce any new concepts, turns what is not exceptions into exceptions.
[1] Java and its offspring's failed experiments in seeing if errors and exceptions could reasonably be considered the same thing excepted.
This is a lot of words, but it's just a miscommunication: when we say exception handling, we mean try/catch. If you want to disagree on the definition or semantics then feel free.
What miscommunication are you speaking of? "Exceptions" was understood to mean "exception handlers" from the beginning. I even expressed that understanding earlier. While you've taken us down some interesting tangents, for whatever strange reason you found it relevant, the core discussion about exception handling has also remained intact.
But you seem to want to avoid talking about it? Let's try one more time: What is it about dead-simple search and replace, without adding any other new features or technical concepts, that turns what is "not exceptions" into something that is "exceptions"? Because I don't understand the difference that makes.
All of the languages are turing complete; the fact that you can make them do the same things with relatively simple transforms isn't actually surprising, it is in fact a natural consequence. Of course, you can do the same thing with nearly any stack unwinding primitive as long as it's general enough like panic/recover.
Language decisions scoped try/catch are not incidental details.
> Not even when listing cons that wouldn't have been there with try-catch.
What would you hope to learn from it? The cons are why you're already not making use of the feature that has existed since the very first release (in most cases that is; there is certainly a time and place for everything — even the standard library uses it sometimes!). Is it that you find it necessary for a third-party to remind you of why you have made your choices? I posit that most developers have a functioning memory that sees that unnecessary.
> But of course that would hurt them and the community in so many levels that they don't want to admit...
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.
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.
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.
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.
> 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.
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.)
(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…
> 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.
> 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.
I understand that the decision could be correct for the situation (ie: if the stated goal is have a proposal with enough support and it was not reached not proceed is correct), that is different that the handling of error as-is is bad (that is the reason the people spend years to solve it)
> (that is the reason the people spend years to solve it)
I don't think anyone actually spend years trying to solve it. It's just that over the years, many people have tried to solve it - each for a grand total of maybe a week or so. If you look at the list, you'll see a lot of different proposal authors: https://seankhliao.com/blog/12020-11-23-go-error-handling-pr...
Most of these do not post any other issues and many of those don't even respond in the discussion to their own proposals.
It's a thing that a lot of people coming to the language get frustrated by, think "here is an obvious way to make this better" and file a (usually half-baked) proposal about. It's not a thing that people spend years of focused effort on to polish into something that works.
Compare that to generics: Not only did Ian file a proposal about that roughly every year. The final design also had over a year of intense discussion, with at least a dozen or two consistent participants (and a hundred or so occasional ones). With at least three or four direct iterations.
Error handling is something that a lot of people care a little about.
This issue contradicts the cited surveys where error handling is identified as the most important issue. Isn't the survey more reliable way to read the community room than a github issue?
It doesn't really contradict the survey all that much. 13% of respondents said that error handling is the biggest issue. That leaves 87%, which can rank anywhere from "it's an issue, but not the biggest" through "it's a minor nuisance" through "I don't care" up to "I actively like the status quo". We can only guess about the distribution.
And yes, I agree that the survey is a better source of data, generally. But I will also say that it intentionally asks as broad a definition of "Go user" as possible. Meaning it also (intentionally) asks people who might use Go every once in a while at work. And a good chunk of respondents are newcomers. These groups are more likely to identify this as a problem. While people who are active on GitHub tend to bias towards people who use it as a daily driver and are much more used to its idioms.
The data is mixed. I fully acknowledge that. But anecdotally, there seems to be a pretty clear pattern that people who come new to the language complain about this, but then get used to it and at the point where they become active in the community, they prefer the status quo. I don't think the experience of newcomers should be dismissed, but I also think it should be acknowledged that it's something most people get used to.
Oh and to be clear: I didn't say "look at this issue, most people clearly prefer the status quo". I just said that given this issue, making the claim that the status quo is objectively bad is hard to justify. I said that its badness is clearly subjective.
That is, I criticized the strength of the original claim. I didn't try to make an equally strong opposite claim.
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.
> 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.
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
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.
For Java systems that I work on for my own account there is a lot of stuffing things like SQL queries into resource files so that I don't have to mess around with quotes and such.
To defy it's reputation for verbosity, Java's lambda syntax is both terse and highly flexible. Sum and product types are possible with records and sealed classes. Pattern matching.
I even found a way to make ad-hoc union types of element types from other packages that does exhaustive switch/case checking. I quickly wrote down a PoC so I wouldn't forget[0]. It needs wrapper types and sealed interfaces in the consuming app/package but is manageable and turned out better than other attempts I'd made.
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.
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...
> 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".
I don't understand this part. Can you give some concrete examples? In my experience, Google Gson and Jackson FasterXML can solve 99.9% of the Java Generic issues that I might have around de/ser.
I could, or you could use google.
Neither of those tools can solve any issue caused by type erasure.
Just to give some examples, the instanceof operator does not work with generic types, it's not possible to instantiate a generic type (can't do a new T()), can't overload methods that differ only in generic parameter type (so List<String> vs List<Integer>) and so on. Some limitations can be worked around with sending around explicit type info (like also sending the Class<T> when using T), reflection etc., but it's cumbersome, and not everything can be solved that way.
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.
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.
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.
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.
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.
Except if your traits are not dyn-compatible. Which I believe a lot of Rust's traits are not. That restriction is specifically why Go does not allow methods to have extra type parameters: To make it possible for the language implementation to choose its own tradeoff between monomorphization and boxing.
So I don't think you can say that this has nothing to do with the type system. Here is a restriction in the Go type system that was specifically introduced to allow a broad range of implementation choices. To avoid being forced to choose slow compilers or slow code: https://research.swtch.com/generic
The Go type system and the way it does generics is directly designed to allow fast compile times.
Yes, but that is now a different runtime cost which Go also didn't want to pay.
The language goes to great pains to give you pretty good control over layout in memory and avoid the "spray of tiny objects on the heap with pointers between them" that you get in Java and most other managed languages.
I think Swift maybe does something more clever with witness tables, but I don't recally exactly how it works.
> I think Swift maybe does something more clever with witness tables, but I don't recally exactly how it works.
Pestov actually wrote a long explanation of what it is that Swift does there[1,2]. And I’m almost sure you’ve already seen it, but it’s been on my reading list forever and I’m hoping that maybe if I can’t get myself to read it, than somebody else will see this comment, get interested and do it.
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.
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.
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_macro2quote and syn before the actual project compilation even starts).
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.
Take a look a the kubernetes source code. It's millions of lines, and almost all of it is generated. In a language like C++ or Rust, the vast majority of it would be template or macro instantiations.
For an apples-to-apples comparison of compilation speed, you should either include the time it takes go generate to run, and the IDE to re-index all the crap it emits, or you should count the number of lines of code in the largest intermediate representation that C++ or Rust has.
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.
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.
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?
> 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.
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 lang...
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.
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?
I didn't say desired. It would work. Do it and if nobody uses it then so be it. Don’t balk and say “well we could but the elite minds in charge have high quibbles with how it would affect the feel of the language in this one absurd edge case, so we won’t”. Just special case the stupid pattern.
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?
> 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.
We have! Several times, in fact. You might recognize those changes by the names Rust, Zig, etc.
But for those who can't, for whatever reason, update their code to work with the substantial language changes, they are interested to see if there is also a solution that otherwise fits into what they've already got in a backwards-compatible way.
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.
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.
"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.
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?
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.
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.
> 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.
> 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.
Returning an error -- or, more accurately, identifying an error and returning an annotation or transformation of that error appropriate for your caller -- is a way of handling it. The cases where, when your code encounters an error, that it can do anything other than this are uncommon.
This goes completely against the golang error-handling mindset.
Error handling is so important, we must dedicate two-thirds of the lines of every golang program to it. It is so important that it must be made a verbose, manual process.
But there's also nothing that can be done about most errors, so we do all this extra work only to bubble errors up to the top of the program. And we do all this work as a human exception-handle to build up a carefully curated manual stack trace that loses all the actually-useful elements of a stack trace like filenames and line numbers.
Handling errors this way is possible in only very brittle and simplistic software.
I mean, you're contradicting your very own argument. If this was the primary/idiomatic way of handling errors... then Go should just go the way of most languages with Try/Catch blocks. If there's no valuable information or control flow to managing errors... then what's the point of forcing that paradigm to be so verbose and explicit in control flow?
> 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!
A type assert/switch is exactly how you implement Error.Is [^0] if you define custom error types. Sure it's preferable to use the interface method in case the error is wrapped, but the point stands. If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
> 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.
I'd argue it's higher than 9% if you're dealing with IO, which most applications will. Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
> If you define errors with Errors.New you use string comparison.
With `Errors.New`, you're expected to provide a human-readable message. By definition, this message may change. Relying on this string comparison is a recipe for later breakages. But even if it worked, this would require documenting the exact error string returned by the function. Have you _ever_ seen a function containing such information in the documentation?
As for `switch x.(type)`, it doesn't support any kind of unwrapping, which means that it's going to fail if someone in the stack just decides to add a `fmt.Errorf` along the way. So you need all the functions in the stack to promise that they're never going to add an annotation detailing what the code was doing when the error was raised. Which is a shame, because `fmt.Errorf` is often a good practice.
I was actually referring to the implementation of errors.Is, which uses string comparison internally if you use the error type returned by errors.New and a type cast or switch if you use a custom type (or the cases where the stdlib defines a custom error type).
> A type assert/switch is exactly how you implement Error.Is [^0]
errors.Is is already implemented in the stdlib, why are you implementing it again?
I know that you can implement it on your custom error type, like your link shows, to customize the behavior of errors.Is. But this is rarely necessary and generally uncommon..
> If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
What? If you want your callers to be able to identify ErrFoo then you're always going to define it as a package-level variable, and when you have a function that needs to return ErrFoo then it will `return ErrFoo` or `return fmt.Errorf("annotation: %w", ErrFoo)` -- and in neither case will callers use string comparison to detect ErrFoo, they'll use errors.Is, if they need to do so in the first place, which is rarely the case.
This is bog-standard conventional and idiomatic stuff, the responsibility of you as the author of a package/module to support, if your consumers are expected to behave differently based on specific errors that your package/module may return.
> Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
Sure, sometimes, rarely, callers need to make decisions based on something more granular than just err != nil. In those minority of cases, they usually just need to call errors.Is to check for error identity, and in the minority of those minority of cases that they need to get even more specific details out of the error to determine what they need to do next, then they use errors.As. And, for that super-minority of situations, then sure, you'd need to define a FooError type, with whatever properties callers would need to get at, and it's likely that type would need to implement an Unwrap() method to yield some underlying wrapped error. But at no point are you, or your callers, doing type-switching on errors, or manual unwrapping, or anything like that. errors.As works with any type that implements `Error() string`, and optionally `Unwrap() error` if it wants to get freaky.
> 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.
I know..! Ignoring an error at a call site is a bug by the caller, that Go requires teams to de-risk via code review, rather than via the compiler. This is well understood and nobody disputes it. And yet all available evidence indicates it's just not that big of a deal and nowhere near the sort of design catastrophe that critics believe it to be. If you don't care or don't believe the data that's fine, everyone knows your position and knows how dumb you think the language is.
Indeed, while not being a fan of how this aspect of Go, I have to admit that it seldom causes issues.
It is, however, part of the reasons for which you cannot attach invariants to types in Go, which is how my brain works, and probably the main reasons for which I do not enjoy working with Go.
Yeah, I mean, Go doesn't see types as particularly special, rather just as one of many tools that software engineers can leverage to ship code that's maintainable and stands the test of time. If your mindset is type-oriented then Go is definitely not the language for you!
> 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.
Let me detail my claim.
Broadly speaking, in programming, there are three kinds of errors:
1. errors that you can do nothing about except crash;
2. errors that you can do nothing about except log;
3. errors that you can do something about (e.g. retry later, stop a different subsystem depending on the error, try something else, inform the user that they have entered a bad url, convert this into a detailed HTTP error, etc.)
Case 1 is served by `panic`. Case 2 is served by `errors.New` and `fmt.Errorf`. Case 3 is served by implementing `error` (a special interface) and `Unwrap` (not an interface at all), then using `errors.As`.
Case 3 is a bit verbose/clumsy (since `Unwrap` is not an interface, you cannot statically assert against it, so you need to write the interface yourself), but you can work with it. However, if you recall, Go did not ship with `Unwrap` or `errors.As`. For the first 8 years of the language, there was simply no way to do this. So the entire ecosystem (including the stdlib) learnt not to do it.
As a consequence, take a random library (including big parts of the stdlib) and you'll find exactly that. Functions that return with `errors.New`, `fmt.Errorf` or just pass `err`, without adding any ability to handle the error. Or sometimes functions that return a custom error (good) but don't document it (bad) or keep it private (bad).
Just as bad, from a (admittedly limited) sample of Go developers I've spoken to, many seem to consider that defining custom errors is black magic. Which I find quite sad, because it's a core part of designing an API.
In comparison, I find that `if err != nil` is not a problem. Repeated patterns in code are a minor annoyance for experienced developers and often a welcome landscape feature for juniors.
Again, you don't need to define a new error type in order to allow callers to do something about it. Almost all of the time, you just need to define an exported ErrFoo variable, and return it, either directly or annotated via e.g. `fmt.Errorf("annotation: %w", ErrFoo)`. Callers can detect ErrFoo via errors.Is and behave accordingly.
`err != nil` is very common, `errors.Is(err, ErrFoo)` is relatively uncommon, and `errors.As(err, &fooError)` is extraordinarily rare.
You're speaking from a position of ignorance of the language and its conventions.
Indeed, you can absolutely handle some cases with combinations of `errors.Is` and `fmt.Errorf` instead of implementing your own error.
The main problem is that, if you recall, `errors.Is` also appeared 8 years after Go 1.0, with the consequences I've mentioned above. Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim 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".
On a more personal touch, as a language designer, I'm not a big fan of taking an entirely different path depending on the kind of information I want to attach to an error. Again, I can live with it. I even understand why it's designed like this. But it irks the minimalist in me :)
> You're speaking from a position of ignorance of the language and its conventions.
This is entirely possible.
I've only released a few applications and libraries in Go, after all. None of my reviewers (or linters) have seen anything wrong with how I handled errors, so I guess so do they? Which suggests that everybody writing Go in my org is in the same position of ignorance. Which... I guess brings me back to the previous points about error-fu being considered black magic by many Go developers?
One of the general difficulties with Go is that it's actually a much more subtle language than it appears (or is marketed as). That's not a problem per se. In fact, that's one of the reasons for which I consider that the design of Go is generally intellectually pleasing. But I find a strong disconnect between two forms of minimalism: the designer's zen minimalism of Go and the bruteforce minimalism of pretty much all the Go code I've seen around, including much of the stdlib, official tutorials and of course unofficial tutorials.
> Indeed, you can absolutely handle some cases with combinations of `errors.Is` and `fmt.Errorf` instead of implementing your own error.
Not "some cases" but "almost all cases". It's a categorical difference.
> Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim 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".
First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types.
But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
> Not "some cases" but "almost all cases". It's a categorical difference.
Good point. But my point remains.
> First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types.
>
> But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
I may misunderstand what you write, but I have the feeling that you are contradicting yourself between these two paragraphs.
I absolutely agree that the code yielding the error cannot know (again, with the exception of panic, but I believe that we agree that this is not part of the scope of our conversation). Which in turn means that every function should document what kind of errors it may return, so that the decision is always delegated to client code. Not just the "relatively few APIs" that you mention in the previous paragraph.
Even `text.Marshal`, which is probably some of the most documented/specified piece of code in the stdlib, doesn't fully specify which errors it may return.
And, again, that's just the stdlib. Take a look at the ecosystem.
> I absolutely agree that the code yielding the error cannot know (again, with the exception of panic, but I believe that we agree that this is not part of the scope of our conversation). Which in turn means that every function should document what kind of errors it may return, so that the decision is always delegated to client code.
As long as the function returns an error at all, then "the decision [as to how to handle a failure] is always delegated to client [caller] code" -- by definition. The caller can always check if err != nil as a baseline boolean evaluation of whether or not the call failed, and act on that boolean condition. If err == nil, we're good; if err != nil, we failed.
What we're discussing here is how much more granularity beyond that baseline boolean condition should be expected from, and guaranteed by, APIs and their documentation. That's a subjective decision, and it's up to the API code/implementation to determine and offer as part of its API contract.
Concretely, callers definitely don't need "every function [to] document what kind of errors it may return" -- that level of detail is only necessary when it's, well, necessary.
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.
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.
You've managed to miss the entire point of using a union: the value is either a success payload or an error value, never both.
You can't encode that mutual exclusivity if you return a std::pair or std::tuple. That's exactly why std::expected, std::variant, or Rust enums exist, to make that constraint explicit in the type system.
Yeah that makes sense. My assertion was definitely incorrect, but also not really what I was trying to describe. My argument is that they are not conceptually different. The implementation is different, in that a union occupies the same memory for either value, but whether they occupy the same memory or not, you have to check the value to determine that is an error. The compiler can force you to handle multiple return values the same way it can force you to check a variant.
> My argument is that they are not conceptually different.
Your argument is nonsensical on its face, it could not be a more different way to compose types.
> The implementation is different
It's also largely irrelevant, the commenter above makes no mention of the memory state, because that's not the point. The point is that the states are exclusive.
> The compiler can force you to handle multiple return values the same way it can force you to check a variant.
No it can not, specifically because it's a product type (or it would be if it were a type anyway), and there is no relation between the fields. At most you can apply some mangy heuristics and hope they don't fuck up to often.
A sum type tells you in no uncertain terms that you can have only one of the types at any given time.
And that's not accounting for the fact that a result is a reified value, which an MRV is not, so it can be passed around and manipulated as any value, and can be operated on as a thing of its own.
> 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.
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.
599 comments
[ 5.7 ms ] story [ 335 ms ] threadI 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.
> For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling
What error are you talking about?
Internal compiler errors are very much the implementation's problem.
UPDATE: See comment below for full error message and a link to the code.
<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?
> 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!
> 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.
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":
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.(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.)
If it was going to be killed by this approach, it would now be dead.
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.
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.
> 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?
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++.
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.
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
- 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?
Even bash has -e :)
What I have learned in this time is: Go is a very good, very high quality, very well designed language. PHP is a terrible, terrible, terrible language -- at least if your mission is creating highly reliable software. If your goal is rapid prototyping of the visual appearance of a webpage, PHP is good for that -- that's what it was originally invented for, back in the 1990s, and it's still the best language for that specific use case.
In PHP, if you are looking at a line of code, it's impossible to tell what happens if an error occurs. There's an error_reporting level that can be changed anywhere else in the codebase at any time. So an error might be thrown in the user's face, or not -- depending on the error_reporting level and what the error is. This can also interact with output buffering. (I guess if the "@" sign is present, at least then you know output of an error message is suppressed.) There's a error_handler function that could be set. Oh but it gets worse! Even if you have the entire codebase and can scour every line of code, and trace every possible execution path, you still can't tell what will happen if an error occurs at the line of code you are looking at -- because there are php.ini settings that come into play!
This is without even mentioning the try/catch exception handling system, which PHP also has, and that you think is such a good thing. So if an error happens on the line you are looking at, execution may switch to somewhere above you in the call stack. But (at least in theory) if you have the complete source code, you can figure out all the places where that might be.
This is an absolute nightmare if you care about reliability. The worst thing you could possibly have in a programming language if you care about reliability is to be unable to tell what happens when an error occurs and reason about the program's behavior.
Contrast this with Go. In Go, I can know what happens if an error occurs because it's right there. There's an "if err != nil {..." block that tells me exactly what happens.
By porting code to Go, I've been able to increase the reliability tremendously. The thing about go, is that using Go doesn't automatically give you reliability -- you have to learn the techniques and tools in Go that give you reliability. But once you do that, the quality of software you can produce is vastly higher.
I've spent the last decade or so learning this. Learning how to structure the code to make maximum use of the compiler's static type checking to catch errors. Making use of the static analysis tools that catch errors. Using a combination of errcheck and staticcheck (mentioned elsewhere in this discussion), you can guarantee every err return value is actually checked and handled. So the oft-cited language flaw of Go allowing you to forget to handle errors becomes a non-issue. Go has built-in support for unit tests and the excellent polymorphism system makes tests easier to make than in PHP. And, getting back to the topic at hand, in Go, I created my own system for handling the err values that get returned so junk errors created by hackers trying to find security vulnerabilities on our server get filtered out, while actual errors that I need to fix get logged and brought swiftly to my attention. Can you do something similar in PHP? In theory, perhaps, but we are to afraid of breaking things to try to massively refactor our ~2 million lines of PHP code in order to do that. So we have various log files and database entries that get clogged with errors, and sometimes we notice errors that we need to fix, but usually we don't. U...
Sure, someone can write a Go library to wrap every low-level function to make sure there's some error handling, maybe with closures and generics it would be quite okay.
In Go if you don't see the error handled you know it's not handled anywhere else. Great? Well, sure .. um, maybe? After all if you want to handle it you need to add error handling there. Consequently your code now is 3x as many lines and ~66% of it is returning errors upward. It's the new Assembly.
(I don't think try-catch is good, I think that PHP's error handling is better despite try-catch.)
Porting to Go (for reliability or otherwise), why? There are other languages out there! Especially if you spent the last decade learning about compile-time checks.
I know that k8s (and tons of now-critical software) is written in Go, and it's not a pretty sight -- and instead of having better abstractions there's NASA-cargo-culting[0]. Linux is written in C. It does not make C a great choice for many reasons. (Go is definitely a better choice than C when it comes to memory safety for example, but I prefer Scala or Rust.)
Facebook added their own typing to PHP (and tellingly called it Hack, of course).
[0] https://github.com/kubernetes/kubernetes/blob/ec2e767e593953...
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.
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.
No. You have not considered what is going on around you.
> A developer uses Result because T and E are exclusive.
A programming language may be designed around that premise, but that is not the case in the real world (faults in the real world are never binary), and it is certainly not aligned with the design of Go.
> The idiomatic Go approach makes no provision for such distinctions at all.
Naturally. It logically can't because T and E are both considered to be always valid per the fundamentals of the language. The idioms emerged as an extension of those core principles. As before, other languages may take different view, but Go isn't those languages.
That's what makes this changing error handling business so hard. If Go were a completely different language then it would be easy to adopt something like monads, but for the Go we have things like that just don't logically fit and it is not yet clear what does.
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.
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.
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.
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).
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.
> 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.
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.
According to 13% of respondents. So yes, it's the "#1 issue", but also not by a huge overwhelming majority or anything.
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.
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.
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
- 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.
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.
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).
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.
Specifically, if Go’s error handling poses a constitutional objection for you, it’s probably just one item in a long list of things that prevent you from using the language. Changing everything to pacify you will take a long time and likely involve many breaking changes, and the end result is likely to be something that does not appeal to Go’s users or even many of the people who shared your objection about error handling but not all of your other objections.
This is not survivorship bias.
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."
"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;"
But now a sort of democracy is required for changes. I’m not sure this is necessary.
At the time the community was pretty fragmented between vanilla GOPATH, vendoring, godep, and one or two others that are escaping my memory. I don't think that meets my criteria for "a lot of consensus".
Probably a better example would be the type alias stuff that was introduced pretty explicitly to support Google's use case without much consultation from the wider community. That caused some kerfuffle as well; however, that also caused the maintainers to change their stance and lean into the community a lot more.
But yes, both of these examples predate any formal "rules of engagement" with the community and things have generally been better since. Moreover, these are the only two examples I can think of where the Go team pushed through some controversial, significant change. The Go team is extremely conservative (which is something I value, for the record), and far more likely to make no change at all even when there is a lot of enthusiasm for some particular change.
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.
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".
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
instead of just [syscall.EINVAL].Here we go, fifth time we're both spelling this one out. This thread is now a meta-self-joke.
> 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
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).
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.
My experience in go was opposite of yours. The original devs (who were long gone) provided no information at all at the error site and I felt lucky even to find the place in the code that produced the error. Unfortunately the "force you to handle errors" idea, while well intentioned, doesn't "force you to provide useful error handling information", making it worse than stack traces by default.
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.
I dream if err, if err dreams me.
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.
What? Survey says 13% mentioned error handling.
And some people actually do prefer it as is.
https://go.dev/blog/survey2024-h1-results
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?
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.
`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.
"&()".
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.
* 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
Also I don't understand how to implement transparent proxies in Go for reactive UI programming.
maybe caps for export is ugly, it's not much different from how python hides with _
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
Imagine explaining these rules to a beginner learning programming.
That was never its purpose and using it that way is in fact misuse.
Name mangling was added to avoid unintentional conflicts in inheritance scenarios. That’s why it’s static, simple, and well documented.
The public/private stuff is mostly useful for publishing modules with sound APIs.
There is no such a thing.
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.
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.
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.
> 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?
> 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.)
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.
This claim isn't true. The annotation-wrapping isn't just about making the returned error 'log-friendlier', it's annotating the error with context-specific information that's useful beyond "log" contexts. More concretely, that kind of annotation is critical if you expect to be able to introspect runtime errors in any way at all -- it's not a "log-friendlier" version of `return err`, it's an important and necessary change vs. `return err` which is in no way perfunctory or whatever.
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.
Did Rust become a clusterfuck like C++?
Is Go as timeless as it was during release?
>The Go team takes community feedback seriously
It feels like reading satire, but it's real.
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'
but this runs just fine (and you will have no idea because of the default 0 value for `y`): this also compiles and runs just fine but again you would have no idea something was wrong 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?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.
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).
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
This bonkers design decision is, as far as I can tell, the underlying infectious cause of nearly every real issue with the language.
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?
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)
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.
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.
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.
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.
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.
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
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).That's clearly not true.
The idea doesn't even make any sense. How could you even begin to handle errors in a meaningful way if you were unable to discern what type the error is?If you’re willing to share, I’m very curious to see a code example of what you mean by this.
I ripped most of it off of someone else, link in the gist
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.
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:
You can also use helper methods on Result (like `map_err`) for inserting explicit conversions between error types:2. Idiomatic go type erases errors, so you're converting from `error` to `error`, hence type-directed conversions are not even remotely an option.
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.
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.
A thread from two days ago bemoans this point:
https://news.ycombinator.com/item?id=44149809
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?
> 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)
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.
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.
- It isn't easily breakpointable.
- It favors "bubbling up" as-is over enriching or handling.
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.
Works especially well in languages that can make assignments in if statements, e.g:
if foo = 42 { }
It’s usually pretty obvious why: eg
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.would be clearer, I think. Seems like it's the same but would color differently in my editor.
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.
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...
...
This clearly can't be solved "just as well" because nobody can figure out how to do it. The second half of your comment alludes to this, but a lot of what makes this hard to solve are pretty inherent to the design of the language, and at this point, there's a pretty large body of empirical evidence showing that there's not going to be a solution that elegantly solves the issue for every possible theoretical case. Even if someone did manage to come up with it, they're literally saying that they wouldn't entertain a proposal for it at this point! I don't understand how you can come away from this thinking it's realistic that this would get solved in some general way.
> 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.
That's exactly the argument for solving this for what you're calling a "narrow" case. Providing syntax just for (T, E) that uses the zero value for T when short-circuiting to return E would improve the situation from a human perspective, even if it meant that to utilize it for more than two return values you need to define a struct for one or both of T or E. The only objections to it that you're raising are entirely from the "computer" perspective of needing to solve the problem in a general fashion, which is not something that needs to be done in order to alleviate the issues for humans.
Fine, but then that means there is no other solution for Go unless you completely change the entire fundamental underpinnings of the language. But, again, if you're going to completely change the language, what's the point? Just use a different language that already has the semantics you seek. There are literally hundreds of them to choose from already.
> That's exactly the argument for solving this for what you're calling a "narrow" case.
Go has, and has had since day one, Java-style exception handlers. While it understandably has all the same tradeoffs as Java exception handling, if you simply need to push a value up the stack, it is there to use. Even the standard library does it when appropriate (e.g. encoding/json). The narrow error case is already covered well enough - at least as well as most other popular languages that have equally settled on Java-style exception handling.
Let me be clear: It is the general case across all types that is sucky. Errors, while revealing, are not the real problem and are merely a distraction.
What's strange to me is that the main reason this seems like the best path forward to me is because it doesn't require large fundamental changes to the language, and I'm skeptical that there's any fix to the underlying issues that you're concerned about that wouldn't require those sorts of changes, but somehow your objections seem to mostly be on the grounds that you also don't want those types of changes. To me, the insistence the entire real problem needs to be solved for anything to be worth doing is in practice incompatible with the requirement not to change the language fundamentally, so the question becomes whether its worth considering changes that don't fully solve what you consider to be the real issue. I think that's where the disconnect is; I'm not really trying to argue for a solution to the problem you're concerned about because I don't consider it to be realistic that it will ever get solved, so I'm arguing that making smaller changes to reduce the impact of the problem without solving it fully would be worthwhile compared only to the status quo rather than some ideal solution that I don't think exists. I'm not trying to say that it's an absolute certainty that there's no way to fix the issues you're concerned about without fundamentally changing the language, and I'd be just as happy as you if I turn out to be wrong! It doesn't really feel like you're rebutting the actual suggestion I'm making though because you're interpreting my claim that there isn't any way to solve the problem generally without changing the language fundamentally as my advocating for those fundamental changes, which is not what I actually think, and from my perspective isn't what I've been saying at all.
I don't see meaningful difference in the syntax as compared to other languages with a similar feature: https://go.dev/play/p/RrO1OrzIPNe How deep are we really going to split hairs here?
If it were commonly used you could clean it up a little, like how the somewhat recently added iterators feature cleaned up what people were already doing with iteration, but in this case since it is so rarely used in the first place, why bother? "If you build it, they will come" is Hollywood fantasy. Unlike this, the use of iterators was already prevalent before the iterators feature was added.
Let's be honest: If it were useful, people would already put up with the above being slightly less than perfect. People will actually put up with a lot of shit when something is useful at its core! But that they are doing this almost never is quite telling.
> What's strange to me is that the main reason this seems like the best path forward to me is because it doesn't require large fundamental changes to the language
Or maybe no changes at all. Would using the above really be so bad from a syntactical point of view? The much bigger problem, and why pretty much all modern languages have moved to returning errors as the defacto solution, is that it exhibits all the same fundamental problems as errors under Java exception handling. That is something syntax cannot fix.
And, well, for the exceptional (pun intended?) cases where Java-style exception handling really its the best option to suit your circumstances: It's there to use already!
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.
Yes, exactly. The unusual thing _should_ look unusual.
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.
> [I] 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.
Clearly not different enough.
Tests are just one tool among many that we use to build and evaluate mental models of behaviour. It's equally possible that the parent commenter noticed unusual behaviour _via_ their tests, and took "ages to debug" precisely _because_ they were misreading the code while trying to understand _why_ the tests were failing. A hypothetical syntax highlighter that flagged up to them "hey, you're doing something unusual here - is that intended?" would have helped them in debugging _alongside_ tests.
If you take the word as gospel, but why should we? It is hard to believe. As shocking as it may be, not everything you read on the internet is true.
Either way, the fact of the matter is that discussion about code is silly without code. Since I have no knowledge of the actual code in question, which has suspiciously been kept a secret for some reason, I'll open the bidding with this: https://go.dev/play/p/xEnGTmJ_57g — From the output alone, you don't think you'd be able to gain a pretty good idea of what the problem might be?
Feel free to update the code with something more real-worldy if you think the contrivedness of it masks what you are trying to talk about. We had to start somewhere.
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.
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".
Go chose not to change the error handling - Nature remained in balance.
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.
The Go team thoroughly explored the design space for seven years and did not find community consensus.
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.
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
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.
That is shorter and keeps the happy path unindented, even if it has additional such constructs, for example
Anyway you can always copy paste it in the normal linear format.
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.
A more refined version of what I originally said would say "conditional branch" instead of "branch", but I'll admit that my original message should have been worded more carefully. I think people understood it, but taken literally it's not a strong argument.
But of course that would hurt them and the community in so many levels that they don't want to admit...
- 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.)
It does have them, though, and always has. Use is even found in the standard library (e.g. encoding/json). They are just not commonly used for this because of the inherit problems with using them in this way as you have already mentioned. But you can. It is a perfectly valid approach where the tradeoffs are acceptable.
But, who knows what the future holds? Ruby in the early days also held the same preference for error values over exceptions... Until Ruby on Rails came along and shifted the prevailing attitude. Perhaps Go will someday have its "Ruby on Rails" moment too.
But I think we'll have to agree to disagree on that one, since there's little to be gained from a long debate about what jargon either does or should subjectively mean. Just trying to explain where I'm coming from.
What is there to debate? An exception, by every definition I have ever encountered, is a data structure that contains runtime information (e.g. a stack trace) to stand in for a compiler error where the compiler was not sufficiently capable of determining the fault at compile time. It couldn't possibly mean anything else in reason.
Of course, we're really talking about "exception handlers", not "exceptions".
> there's no try, no catch, and no throw, and no equivalent to any of those.
There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
Think about it. Go could easily provide syntax sugar that replaces `try { throw() } catch (err) {}` with `try(func() { throw() }).catch(func(err) {})`. That would truly satisfy your requirements in every way. But what, specially, in that simple search and replace operation says "exceptions" (meaning exception handlers)?
> C also has setjmp/longjmp which can be used in similar ways, but I wouldn't call that exception handling either.
Agreed. You could conceivably craft your own exceptions to carry through the use of setjmp/longjmp, but that wouldn't be a language feature. However, Go does have an exception structure as a built-in.
True to my word, I won't argue over the definition itself.
[1]: https://en.wikipedia.org/wiki/Exception_handling_(programmin...
P.S.:
> There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do. For example, Go doesn't have pattern matching; implementing an equivalent feature with closures does not make this any less true.
What's that old adage? I think it goes something like "The wiki is always accurate—except when it’s about something you know personally." If you don't enough about the topic to discuss it yourself, what are you doing here?
> Also, obviously when we say "exceptions" in a programming language, we're definitely talking about "exception handling"
Not necessarily. Often it is important to discuss the data structure and not the control flow. Strictly, "exception" refers to either the broad concept of exceptional circumstances (i.e. programmer error) or the data structure to represent it. "Exception" being short for "exception handling" where context is clear is fine, but be sure context is clear if you want to go down that road – unless you like confusing others, I suppose.
> well the Go error object is an "exception"
You mean the error interface? That's not an exception. It's just a plain old interface; literally `type error interface { Error() string }`. In fact, the only reason it gained special keyword status is because it being part of the standard library, where it was originally defined in early versions, caused cyclical import headaches. If Go supported circular imports, it would be a standard library definition instead of a keyword today.
The underlying data structure produced when calling panic is an exception, though. It carries the typical payload you'd expect in an exception, like the stack trace.
Of course, errors and exceptions are conceptually very different. Errors are for things that happen in the environment – invalid input, network down, hard drive crash, etc. Exceptions are for programmer mistakes – faults that could have theoretically been detected at compile time if you had a sufficiently advanced compiler. Obviously you can overload exceptions to carry unexceptional information (as you can overload errors to carry exceptional information), and a pragmatist will from time to time, but that's not the intent for such a feature[1].
> To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do.
Aside from the fact that there is actually an exception involved. Again, while you might be able to hand roll your own exception data structure in C, it does not provide it for you like Go does. If setjmp/longjmp were paired with an exception data structure of the box, it would reasonably considered exceptions, naturally.
However, the second bit was the real meat of that. A tiny bit of search and replace and you have a system that is effectively indistinguishable from exception handling in languages like Java, Javascript, etc. You haven't explained what about that search and replace, that does not introduce any other new language features or introduce any new concepts, turns what is not exceptions into exceptions.
[1] Java and its offspring's failed experiments in seeing if errors and exceptions could reasonably be considered the same thing excepted.
But you seem to want to avoid talking about it? Let's try one more time: What is it about dead-simple search and replace, without adding any other new features or technical concepts, that turns what is "not exceptions" into something that is "exceptions"? Because I don't understand the difference that makes.
Language decisions scoped try/catch are not incidental details.
Go already has that, of course: https://go.dev/play/p/RrO1OrzIPNe
> Not even when listing cons that wouldn't have been there with try-catch.
What would you hope to learn from it? The cons are why you're already not making use of the feature that has existed since the very first release (in most cases that is; there is certainly a time and place for everything — even the standard library uses it sometimes!). Is it that you find it necessary for a third-party to remind you of why you have made your choices? I posit that most developers have a functioning memory that sees that unnecessary.
> But of course that would hurt them and the community in so many levels that they don't want to admit...
You may not have thought this through...
You can put that in-band, with something like:
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:
But that's where you start to see things hidden.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.
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.
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.)
(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…
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.
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.
I don't think anyone actually spend years trying to solve it. It's just that over the years, many people have tried to solve it - each for a grand total of maybe a week or so. If you look at the list, you'll see a lot of different proposal authors: https://seankhliao.com/blog/12020-11-23-go-error-handling-pr... Most of these do not post any other issues and many of those don't even respond in the discussion to their own proposals.
It's a thing that a lot of people coming to the language get frustrated by, think "here is an obvious way to make this better" and file a (usually half-baked) proposal about. It's not a thing that people spend years of focused effort on to polish into something that works.
Compare that to generics: Not only did Ian file a proposal about that roughly every year. The final design also had over a year of intense discussion, with at least a dozen or two consistent participants (and a hundred or so occasional ones). With at least three or four direct iterations.
Error handling is something that a lot of people care a little about.
And yes, I agree that the survey is a better source of data, generally. But I will also say that it intentionally asks as broad a definition of "Go user" as possible. Meaning it also (intentionally) asks people who might use Go every once in a while at work. And a good chunk of respondents are newcomers. These groups are more likely to identify this as a problem. While people who are active on GitHub tend to bias towards people who use it as a daily driver and are much more used to its idioms.
The data is mixed. I fully acknowledge that. But anecdotally, there seems to be a pretty clear pattern that people who come new to the language complain about this, but then get used to it and at the point where they become active in the community, they prefer the status quo. I don't think the experience of newcomers should be dismissed, but I also think it should be acknowledged that it's something most people get used to.
That is, I criticized the strength of the original claim. I didn't try to make an equally strong opposite claim.
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.
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.
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.
https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_sy...
To defy it's reputation for verbosity, Java's lambda syntax is both terse and highly flexible. Sum and product types are possible with records and sealed classes. Pattern matching.
[0] https://github.com/karmakaze/AdHocUnion
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/
Just to give some examples, the instanceof operator does not work with generic types, it's not possible to instantiate a generic type (can't do a new T()), can't overload methods that differ only in generic parameter type (so List<String> vs List<Integer>) and so on. Some limitations can be worked around with sending around explicit type info (like also sending the Class<T> when using T), reflection etc., but it's cumbersome, and not everything can be solved that way.
[1] Okay fine, you can fake it with enough SRTPs, but Don Syme will come and burn your house down.
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.
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.
So I don't think you can say that this has nothing to do with the type system. Here is a restriction in the Go type system that was specifically introduced to allow a broad range of implementation choices. To avoid being forced to choose slow compilers or slow code: https://research.swtch.com/generic
The Go type system and the way it does generics is directly designed to allow fast compile times.
Yes, but that is now a different runtime cost which Go also didn't want to pay.
The language goes to great pains to give you pretty good control over layout in memory and avoid the "spray of tiny objects on the heap with pointers between them" that you get in Java and most other managed languages.
I think Swift maybe does something more clever with witness tables, but I don't recally exactly how it works.
It's not an easy problem.
Pestov actually wrote a long explanation of what it is that Swift does there[1,2]. And I’m almost sure you’ve already seen it, but it’s been on my reading list forever and I’m hoping that maybe if I can’t get myself to read it, than somebody else will see this comment, get interested and do it.
[1] http://download.swift.org/docs/assets/generics.pdf
[2] https://github.com/apple/swift/tree/main/docs/Generics
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.
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).
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.
For an apples-to-apples comparison of compilation speed, you should either include the time it takes go generate to run, and the IDE to re-index all the crap it emits, or you should count the number of lines of code in the largest intermediate representation that C++ or Rust has.
But that would be unfair to the very design choice of omitting metaprogramming while exposing the go/ast library to users to foster code generation.
OCaml and Haskell don't suffer from similar pain points, because interpreters, and JIT like tooling is also available.
One can happily develop with one toolchain, and press the red button for an optimised compiled build, when it actually matters.
Also both Swift and Rust have made the mistake to make used of the compiler backend that everyone avoids when they care about build performance, LLVM.
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.
Am I missing something? Is this really a good idea for a language that can't express monads naturally?
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.
> 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 lang...
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.
Source: I'm one of the people who designed it.
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.
But for those who can't, for whatever reason, update their code to work with the substantial language changes, they are interested to see if there is also a solution that otherwise fits into what they've already got in a backwards-compatible way.
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.
Assuming that all complainants are just idiots is purely misinformed and quite frankly a bit of gaslighting.
Yes, non-experts can have valid criticisms but more often than not they're too ignorant to even understand what trade-offs are involved.
is the entire go community this toxically ignorant?
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.
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.
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.
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.
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.
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.
Error handling is so important, we must dedicate two-thirds of the lines of every golang program to it. It is so important that it must be made a verbose, manual process.
But there's also nothing that can be done about most errors, so we do all this extra work only to bubble errors up to the top of the program. And we do all this work as a human exception-handle to build up a carefully curated manual stack trace that loses all the actually-useful elements of a stack trace like filenames and line numbers.
Handling errors this way is possible in only very brittle and simplistic software.
I mean, you're contradicting your very own argument. If this was the primary/idiomatic way of handling errors... then Go should just go the way of most languages with Try/Catch blocks. If there's no valuable information or control flow to managing errors... then what's the point of forcing that paradigm to be so verbose and explicit in control flow?
None.
A type assert/switch is exactly how you implement Error.Is [^0] if you define custom error types. Sure it's preferable to use the interface method in case the error is wrapped, but the point stands. If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
> 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.
I'd argue it's higher than 9% if you're dealing with IO, which most applications will. Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
[0]: The documentations own example of implementing Error.Is uses a switch. https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/...
It happens to be a syscall interface so errors are reported as numbers.
With `Errors.New`, you're expected to provide a human-readable message. By definition, this message may change. Relying on this string comparison is a recipe for later breakages. But even if it worked, this would require documenting the exact error string returned by the function. Have you _ever_ seen a function containing such information in the documentation?
As for `switch x.(type)`, it doesn't support any kind of unwrapping, which means that it's going to fail if someone in the stack just decides to add a `fmt.Errorf` along the way. So you need all the functions in the stack to promise that they're never going to add an annotation detailing what the code was doing when the error was raised. Which is a shame, because `fmt.Errorf` is often a good practice.
https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/...
The answer is that errors.New just wraps the error message in an errorString struct, and the second line of `is` is a string comparison.
And as you demonstrate I could have tested it easily enough to confirm I was reading the code correctly...
errors.Is is already implemented in the stdlib, why are you implementing it again?
I know that you can implement it on your custom error type, like your link shows, to customize the behavior of errors.Is. But this is rarely necessary and generally uncommon..
> If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
What? If you want your callers to be able to identify ErrFoo then you're always going to define it as a package-level variable, and when you have a function that needs to return ErrFoo then it will `return ErrFoo` or `return fmt.Errorf("annotation: %w", ErrFoo)` -- and in neither case will callers use string comparison to detect ErrFoo, they'll use errors.Is, if they need to do so in the first place, which is rarely the case.
This is bog-standard conventional and idiomatic stuff, the responsibility of you as the author of a package/module to support, if your consumers are expected to behave differently based on specific errors that your package/module may return.
> Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
Sure, sometimes, rarely, callers need to make decisions based on something more granular than just err != nil. In those minority of cases, they usually just need to call errors.Is to check for error identity, and in the minority of those minority of cases that they need to get even more specific details out of the error to determine what they need to do next, then they use errors.As. And, for that super-minority of situations, then sure, you'd need to define a FooError type, with whatever properties callers would need to get at, and it's likely that type would need to implement an Unwrap() method to yield some underlying wrapped error. But at no point are you, or your callers, doing type-switching on errors, or manual unwrapping, or anything like that. errors.As works with any type that implements `Error() string`, and optionally `Unwrap() error` if it wants to get freaky.
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.
Indeed, while not being a fan of how this aspect of Go, I have to admit that it seldom causes issues.
It is, however, part of the reasons for which you cannot attach invariants to types in Go, which is how my brain works, and probably the main reasons for which I do not enjoy working with Go.
Where is this evidence? Where is the data that I am supposed to believe?
Let me detail my claim.
Broadly speaking, in programming, there are three kinds of errors:
1. errors that you can do nothing about except crash;
2. errors that you can do nothing about except log;
3. errors that you can do something about (e.g. retry later, stop a different subsystem depending on the error, try something else, inform the user that they have entered a bad url, convert this into a detailed HTTP error, etc.)
Case 1 is served by `panic`. Case 2 is served by `errors.New` and `fmt.Errorf`. Case 3 is served by implementing `error` (a special interface) and `Unwrap` (not an interface at all), then using `errors.As`.
Case 3 is a bit verbose/clumsy (since `Unwrap` is not an interface, you cannot statically assert against it, so you need to write the interface yourself), but you can work with it. However, if you recall, Go did not ship with `Unwrap` or `errors.As`. For the first 8 years of the language, there was simply no way to do this. So the entire ecosystem (including the stdlib) learnt not to do it.
As a consequence, take a random library (including big parts of the stdlib) and you'll find exactly that. Functions that return with `errors.New`, `fmt.Errorf` or just pass `err`, without adding any ability to handle the error. Or sometimes functions that return a custom error (good) but don't document it (bad) or keep it private (bad).
Just as bad, from a (admittedly limited) sample of Go developers I've spoken to, many seem to consider that defining custom errors is black magic. Which I find quite sad, because it's a core part of designing an API.
In comparison, I find that `if err != nil` is not a problem. Repeated patterns in code are a minor annoyance for experienced developers and often a welcome landscape feature for juniors.
`err != nil` is very common, `errors.Is(err, ErrFoo)` is relatively uncommon, and `errors.As(err, &fooError)` is extraordinarily rare.
You're speaking from a position of ignorance of the language and its conventions.
The main problem is that, if you recall, `errors.Is` also appeared 8 years after Go 1.0, with the consequences I've mentioned above. Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim 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".
On a more personal touch, as a language designer, I'm not a big fan of taking an entirely different path depending on the kind of information I want to attach to an error. Again, I can live with it. I even understand why it's designed like this. But it irks the minimalist in me :)
> You're speaking from a position of ignorance of the language and its conventions.
This is entirely possible.
I've only released a few applications and libraries in Go, after all. None of my reviewers (or linters) have seen anything wrong with how I handled errors, so I guess so do they? Which suggests that everybody writing Go in my org is in the same position of ignorance. Which... I guess brings me back to the previous points about error-fu being considered black magic by many Go developers?
One of the general difficulties with Go is that it's actually a much more subtle language than it appears (or is marketed as). That's not a problem per se. In fact, that's one of the reasons for which I consider that the design of Go is generally intellectually pleasing. But I find a strong disconnect between two forms of minimalism: the designer's zen minimalism of Go and the bruteforce minimalism of pretty much all the Go code I've seen around, including much of the stdlib, official tutorials and of course unofficial tutorials.
Not "some cases" but "almost all cases". It's a categorical difference.
> Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim 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".
First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types.
But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
Good point. But my point remains.
> First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types. > > But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
I may misunderstand what you write, but I have the feeling that you are contradicting yourself between these two paragraphs.
I absolutely agree that the code yielding the error cannot know (again, with the exception of panic, but I believe that we agree that this is not part of the scope of our conversation). Which in turn means that every function should document what kind of errors it may return, so that the decision is always delegated to client code. Not just the "relatively few APIs" that you mention in the previous paragraph.
Even `text.Marshal`, which is probably some of the most documented/specified piece of code in the stdlib, doesn't fully specify which errors it may return.
And, again, that's just the stdlib. Take a look at the ecosystem.
As long as the function returns an error at all, then "the decision [as to how to handle a failure] is always delegated to client [caller] code" -- by definition. The caller can always check if err != nil as a baseline boolean evaluation of whether or not the call failed, and act on that boolean condition. If err == nil, we're good; if err != nil, we failed.
What we're discussing here is how much more granularity beyond that baseline boolean condition should be expected from, and guaranteed by, APIs and their documentation. That's a subjective decision, and it's up to the API code/implementation to determine and offer as part of its API contract.
Concretely, callers definitely don't need "every function [to] document what kind of errors it may return" -- that level of detail is only necessary when it's, well, necessary.
The idea that "the happy path is the most common" is a total lie.
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:
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.
You can't encode that mutual exclusivity if you return a std::pair or std::tuple. That's exactly why std::expected, std::variant, or Rust enums exist, to make that constraint explicit in the type system.
Your argument is nonsensical on its face, it could not be a more different way to compose types.
> The implementation is different
It's also largely irrelevant, the commenter above makes no mention of the memory state, because that's not the point. The point is that the states are exclusive.
> The compiler can force you to handle multiple return values the same way it can force you to check a variant.
No it can not, specifically because it's a product type (or it would be if it were a type anyway), and there is no relation between the fields. At most you can apply some mangy heuristics and hope they don't fuck up to often.
A sum type tells you in no uncertain terms that you can have only one of the types at any given time.
And that's not accounting for the fact that a result is a reified value, which an MRV is not, so it can be passed around and manipulated as any value, and can be operated on as a thing of its own.
That is certainly not how your comment comes across.
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.