(On | No) Syntactic Support for Error Handling
go.dev
go.dev
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.
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.
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.
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.
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.
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.
> blunder made by Java
For normies, what is wrong with Java generics? (Do the same complaints apply to C# generics?) I came from C++ to Java, and I found Java generics pretty easy to use. I'm not interested in what "PL (programming language) people" have to say about it. They dislike all generic/parametric polymorphism implementations except their pet language that no one uses. I'm interested in practical things that work and are easy for normies to learn and use well. > Even the Java team themselves warned the Go team to not make that mistake.
Do you have a source for that?[1] Okay fine, you can fake it with enough SRTPs, but Don Syme will come and burn your house down.
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.
Source: I'm one of the people who designed it.
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.
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.
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.
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…
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.
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.
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.
The idea that "the happy path is the most common" is a total lie.
a + b
CAN fail. But HOW that is the question!So, errors are everywhere. And you must commit to a way to handle it and no is not possible, like no, not possible to satisfy all the competing ideas about it.
So there is not viable to ask the community about it, because:
a + b
CAN fail. But HOW change by different reasons. And there is not possible to have a single solution for it, precisely because the different reasons.So, you pick a side and that is.
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.
There is no such a thing.
Also I don't understand how to implement transparent proxies in Go for reactive UI programming.
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?
What? Survey says 13% mentioned error handling.
And some people actually do prefer it as is.
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.
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.
if err == nil { // inverted
return err
}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.
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.
This is where languages diverge. Many languages use exceptions to throw the error until someone explicitly catches it and you have a stack trace of sorts. This might tell you where the error was thrown but doesn't provide a lot of helpful insight all of the time. In Go, I like how I can have some options that I always must choose from when writing code:
1. Ignore the error and proceed onward (`foo, _ := doSomething()`)
2. Handle the error by ending early, but provide no meaningful information (`return nil, err`)
3. Handle the error by returning early with helpful context (return a general wrapped error)
4. Handle the error by interpreting the error we received and branching differently on it. Perhaps our database couldn't find a row to alter, so our service layer must return a not found error which gets reflected in our API as a 404. Perhaps our idempotent deletion function encountered a not found error, and interprets that as a success.
In Go 2, or another language, I think the only changes I'd like to see are a `Result<Value, Failure>` type as opposed to nillable tuples (a la Rust/Swift), along with better-typed and enumerated error types as opposed to always using `error` directly to help with error type discoverability and enumeration.
This would fit well for Go 2 (or a new language) because adding Result types on top of Go 1's entrenched idiomatic tuple returns adds multiple ways to do the same thing, which creates confusion and division on Go 1 code.
A policy of handling errors usually ends up turning into a policy of wrapping errors and returning them up the stack instead. A lot of busywork.
And this is exactly where Go fails, because it allows you to completely ignore the error, which will lead to a crash.
I'm a bit baffled that you correctly identified that this is a requirement to produce robust software and yet, you like Go's error handling approach...
Note that ignoring errors doesn't necessarily lead ti a crash; there are plenty of functions where an error won't ever happen in practice, either because preconditions are checked by the program before the function call or because the function's implementation has changed and the error return is vestigal.
No it won't. It could lead to a crash or some other nasty bug, but this is absolutely not a fact you can design around, because it's not always true.
[1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo
Hell, you can mostly replicate Gos "error handling" in any language with generics and probably end up with nicer code.
If your answer is "JavaScript" or "Python", well, that's the common pattern.
Whereas in Go, the error is visible everywhere. As a developer I see its path more easily since it's always there, and so I have a better mind to handle it right there.
Additionally, it's less easy to group errors together. A try/catch with multiple throwable functions catches an error...which function threw it though? If you want to actually handle an error, I'd prefer handling it from a particular function and not guessing which it came from.
Java with type-checked exceptions is nice. I wish Swift did that a bit better.
- 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. Usually we don't know anything is wrong until we get customer complaints on the Help Desk.
Anyway, I'm glad the Go team rejected the idea of changing Go's error handling. Is Go's error handling verbose, tedious, and repetitive? Yes. But it's also unambiguous and gives you total control over how your program responds when an error occurs, which is exactly what you want if you want to write software with the maximum reliability.
Don't just take my word for it. Notice how lots of "cloud infrastructure" is written in Go. Kubernetes is written in Go, Docker is written in Go, Terraform is written in Go, etc.
Yes, I know some people, like Facebook, have managed to write decently reliable code in PHP. But I suspect Facebook has thrown massive amounts of dollars and people at the problem. Various other PHP projects, like certain website creation systems that shall not be named, have a reputation for being buggy.
Sorry, I couldn't let you say PHP has better error handling without a response. It may be "better" for having more compact, prettier looking code. But it's not better for writing actually reliable software. If you want to write actually reliable software, Go is way better.
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.
How about:
- Errors can be dropped silently or accidentally ignored
- function call results cannot be stored or passed around easily due to not being values
- errors.Is being necessary and the whole thing with 'nested' errors being a strange runtime thing that interacts poorly with the type system
- switching on errors being hard
- usage of sentinel values in the standard library
- poor interactions with generics making packages such as errgroup necessary
Did I miss anything?
I don't believe this claim is made anywhere.
We've decided that we are not going to make any further attempts to change the syntax of error handling in the foreseeable future. That frees up attention to consider other issues (with errors or otherwise).
We're both Googlers here and this is so disappointing to be let down again by the Go team.
[...]
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
[...]
It's so funny to me to call "manually supplying stack traces" as "handling an error". By the Go team's definition of handling errors, exceptions* "automatically handle errors for you".* in any language except C++, of course
Do I need clear and useful things? Maybe not. Would I like to have them anyway? Yes.
Years ago, I configured a Java project's logging framework to automatically exclude all the "uninteresting" frames in stack traces. It was beautiful. Every stack trace showed just the path taken through our application. And we could see the stack of "caused-by" exceptions, and common frames (across exceptions) were automatically cut out, too.
Granted, I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features...
And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.
> the wrapping is very greppable when done well
Yeah, that's my other problem with it. _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`). (Or use `%v` if I don't want to wrap. Yet another little nuance I find myself needing to explain to junior devs over and over. And don't get me started about putting that in the `fmt` package.)
So anyway, I've seen monstrosities of errors where there were 6+ "statement: statement: statement: statement: statement: final error" that felt like a dark comedy. I've also seen very high-level errors where I dearly wished for some intermediate context, but instead just had "failed to do a thing: EOF".
That all being said, stack traces are really expensive. So, you end up with some "fun" optimizations: https://stackoverflow.com/questions/58696093/when-does-jvm-s...
Easy. Always wrap. Wrap with what you were doing when the error occurred.
> I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features... And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.
Yeah, despite the proliferation of IDEs for Go in recent years, Go has traditionally been pretty anti- big-iron IDE.
The argument for explicit error values is often something like "it encourages people to actually handle their errors, rather than ignoring them". And on the face of it, this has some merit: we've all seen code that assumes an HTTP request can't fail, and now a small timeout crashes the entire backup procedure or whatever.
But if "handle the error" simply means "decorate it with a trace and return it", then exceptions already do this, then you're really admitting that there is no fundamental difference from a exception, because this is exactly what exceptions do, all on their own. Sure, they produce less useful traces, but that's usually a tiny difference. After all, the argument wasn't "you'll get better stack traces than exceptions give you", it was "people will be more careful to handle errors".
This is also relevant, because if the goal is to get better error traces, that can also be done with exceptions, with just some small improvements to syntax and semantics (e.g. add syntax for decorating a call site with user supplied context that will get included in any exception bubbled from it; add support in an exception to only print non-library stack frames, add support in the language to declare certain variables as "important" and have them auto-included in stack traces - many ideas).
Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with. Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto. Can this method fail? Who knows! What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it.
That's a different discussion entirely. And even so, whether any statement can terminate early should not be very relevant: that's why we have try-with-resources/finally/defer and other similar mechanisms.
> Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto.
No, they are not, in any language with exceptions except Common Lisp and Windows SEH. Exceptions in all common languages are early returns, they return to the calling function. Of course, if the calling function doesn't catch them, it will also return early to its calling function. Tracing where control flow will continue after an exception is thrown is exactly equivalent to tracing where control flow will continue after an error is returned in Go.
> Can this method fail? Who knows!
Java has checked exceptions to explicitly mark functions that can throw. While there were some problems with that, especially around the lack of generic exceptions support, this seems like the right way to go, and it mostly got a bad rep simply because of a different zeitgeist in programming at the time.
> What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it.
This is exactly the same in Go, where every single function returns `error`, and there is very rarely any documentation to say what types of errors it might actually return; and virtually all Go code does "if err != nil", which is exactly the same as catch(Exception). Not to mention that most errors used in most Go code are fmt.Errorf, so they don't carry any type information to begin with.
Go could just add an equivalent of `with` clause, which would basically continue with functions as long as error is nil and have an error handling clause at the bottom.
Go is fascinating in how long it holds out on some of the most basic, obviously valuable constructs (generics, error handling, package management) because The Community cannot agree.
- Generics took 13 years from the open source release.
- 16 years in there isn’t error handling.
- Package management took about 9 years.
There’s value to deliberation and there’s value to shipping. My guess is that the people writing 900 GH comments would still write Go and be better off by the language having something vs. kicking the can down the road.
But meanwhile it's just perfectly idiomatic Erlang and Elixir, none of that baggage required. (In fact, the sum types are vastly more powerful than in the ML lineage - they're open.)
I only see this blurb in a linked article:
> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.
But I fail to see how having convenience equates to ignoring the error. Thats basically half of my problem with Go's approach, that nothing enforces anything about the result and only minimally enforces checking the error. eg this results in 'declared and not used: err'
x, err := strconv.Atoi("123")
fmt.Println("result:", x)
but this runs just fine (and you will have no idea because of the default 0 value for `y`): x, err := strconv.Atoi("123")
if err != nil {
panic(err)
}
y, err := strconv.Atoi("1234")
fmt.Println("result:", x, y)
this also compiles and runs just fine but again you would have no idea something was wrong x, err := strconv.Atoi("123")
if err != nil {
}
fmt.Println("result:", x)
Making the return be `result` _enforces_ that you have to make a decision. Who cares if someone yolos a `!` or conveniently uses `?` but doesnt handle the error case. Are you going to forbid `panic` too?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.
This bonkers design decision is, as far as I can tell, the underlying infectious cause of nearly every real issue with the language.
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:
#[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)?;
// ...
}A thread from two days ago bemoans this point:
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?
Idiomatic Go type-erases error types into `error`, when there is even a known type in the first place.
Thus `From` is not a consideration, because the only `From` you need is
impl<'a, E> From<E> for Box<dyn Error + 'a>
where
E: Error + 'a,
and that means you can just build that in and nothing else (and it's really already built-in by the implicit upcasting of values into interfaces).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)
- It isn't easily breakpointable.
- It favors "bubbling up" as-is over enriching or handling.
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 wrongOkay, 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)
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
Languages with stack traces gives this to you for free, in Go, you need to implement it every time. OK, you may be disciplined developer where you always augment the error with the details but not all the team members have the same discipline.
Also the best thing about stack traces is that it gives you the path to the error. If the error is happened in a method that is called from multiple places, with stack traces, you immediately know the call path.
I worked as a sysadmin/SRE style for many years and I had to solve many problems, so I have plenty of experience in troubleshooting and problem solving. When I worked with stack traces, solving easy problems was taking only 1-2 minutes because the problems were obvious, but with Go, even easy problems takes more time because some people just don't augment the errors and use same error messages which makes it a detective work to solve it.
a, err := foo()
b, err := bar()
if err != nil { // oops, forgot to handle foo()'s err }
This is the illusion of safe error handling.For example instead of
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
they could have something like this: func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a)
y <- strconv.Atoi(b)
} yield fmt.Println("result:", x + y)
}
which desugars to: func printSum(a, b string) result[error, unit] {
return strconv.Atoi(a).flatMap(func(x string) result[error, unit] {
return strconv.Atoi(b).map(func(y string) unit {
return fmt.Println("result:", x + y)
}
}
}
and unlike ad-hoc solutions this one bit of syntax sugar, where for comprehensions become invocations of map, flatMap, and filter would handle errors, goroutines, channels, generators, lists, loops, and more, because monads are pervasive: https://philipnilsson.github.io/Badness10k/escaping-hell-wit...How do you do that with this suggestion?
Did anyone propose this in one of the many error handling proposals?
Could in some purely theoretical way, but this is pretty much exactly the same, minor syntax differences aside, as virtually every other failed proposal that has been made around this. It would be useless in practice.
In the real world it would have to look more like this...
func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a) else (err error) {
details := collectDetails(a, b)
stop firstConvError{err, details}
}
y <- strconv.Atoi(b) else (err error) {
stop secondConvError{err}
}
} yield fmt.Println("result:", x + y)
}
or maybe something like this func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a)
y <- strconv.Atoi(b)
} yield fmt.Println("result:", x + y)
}
handle(printSum, "x", func(vars map[string]any, err error) {
details := collectDetails(vars["a"].(string), vars["b"].(string))
return firstConvError{err, details}
}
handle(printSum, "y", func(vars map[string]any, err error) {
return secondConvError{err}
}
...because in that real world nobody just blindly returns errors like your contrived example shows[1]. There are a myriad of obvious (and many not so obvious) problems with that.Once you start dealing with the realities of the real world, it is not clear how your approach is any better. It is pretty much exactly the same as what we have now. In fact, it is arguably worse as the different syntax doesn't add anything except unnecessary complexity and confusion. Which is largely the same reason why all those aforementioned proposals failed. Like the linked article states, syntax is not the problem. The problem is that nobody to date knows how to solve it conceptually without completely changing the fundamentals of the language or just doing what you did, which is pointless.
And there is another obvious problem with your code: result[error, unit], while perfectly fitting in other languages designed with that idea in mind, is logically incorrect in the context of Go. They are not dependent variables. It doesn't make sense to make them dependent. For it to make sense, you would, as before, have to completely change the fundamentals of the language. And at that point you have a band new language, making any discussion about Go moot. That said, I think the rest of your idea could be reasonably grafted onto the (T, error) idiom just as easily. However, it still fails on other problems, so...
[1] Which, I will add, is not just speculation. The Go team actually collected data about this as part of their due diligence. One of the earlier proposals was on the cusp of acceptance, but in the end it wasn't clear who – outside of contrived HN comments – would ever use it due to the limitations spoken of above, thus it ultimately was rejected on that basis.
I’m actually kind of surprised that it’s the top complaint among Go devs. I always thought it was more something that people who don’t use Go much complain about.
My personal pet issue is lack of strict null checks—and I’m similarly surprised this doesn’t get more discussion. It’s a way bigger problem in practice than error handling. It makes programs crash in production all the time, whereas error handling is basically just a question of syntax sugar. Please just give me a way to mark a field in a struct required so the compiler can eliminate nil dereference panics as a class of error. It’s opt-in like generics, so I don’t see why it would be controversial to anyone?
It's too easy to accidentally write `if err == nil` instead of `if err != nil`. I have even seen LLMs erroneously generate the first instead of the latter. And since it's such a tiny difference and the code is riddled with `if err != nil`, it's hard to catch at review time.
Second, you're not forced by the language to do anything with the error at all. There are cases where `err` is used in a function that not handling the `err` return value from a specific function silently compiles. E.g.
x, err := strconv.Atoi(s1)
if err != nil {
panic(err)
}
y, err := strconv.Atoi(s2)
fmt.Println(x, y)
I think accidentally allowing such bugs, and making them hard to spot, is a serious design flaw in the language.It "breaks" the language in fundamental ways — much more fundamental than syntactic sugar for error handling — by making zero values and thus zero initialisation invalid.
You even get this as a fun interaction with generics:
func Zero[T any]() T {
var v T
return v
}Even the article considers "handling" an error to be synonymous with "Adding more text and bubbling it up"!
You can also add additional context to the error before bubbling it up. But yes, that part of the point. Instead of bubbling them up, the programmer should instead reflect on whether it is better than just log and proceed, or completely swallow them. This is what error handling is about.
You can't bubble up an exception, it's done automatically. That's a very important distinction. You can't make the decision to bubble up or not, because you do not have the required information - you don't know whether an exception can be thrown or not at any point. Therefore, you can't say you're making a decision at all.
Explicit error allows you to be able to make the decision.
1. The very fact that adding more text isn't really any more verbose than not encourages you to add more text, making errors more informative.
2. A non-negligible amount of times you do something else: carry on, or do something specific based on what kind of error it was. For instance, ignore an error if it's in a certain class; or create the file if it didn't exist; and so on.
Forcing the error handling doesn't seem to me that different than forcing you to explicitly cast between (say) int and int64. Part of me is annoyed with that too, but then I have PTSD flashbacks from 20 years of C programming and appreciate it.
Go is very readable in my experience. I'd like to keep it that way.
He also made the point that if you have two ways of coding something, then you have to choose every time. I've noticed that people disagree about which way is best. If there were only one way, then all of the same problems would be solved with the same amount of effort, but without any of the disagreement or personal deliberation.
Maybe Go should have exceptions beyond what panic/recover became, or maybe there should be a "?" operator, or maybe there should be a "check" expression, or some other new syntax.
Or maybe Go code should be filled with "if" statements that just return the error if it's not nil.
I've worked with a fair amount of Go code, but not enough that I am too bothered by which pattern is chosen.
On the other hand, if you spend more than a few weeks full time reading and editing Go code, you could easily learn a lot of syntax without issue. If you spend most of your career writing in a language, then you become familiar with the historical vogues that surrounded the addition of new language features and popular libraries.
There's something to be said for freezing the core language.
v, err := foo.Open()
// …
defer func() {
if closeErr := v.Close(); closeErr != nil {
err = fmt.Errorf("while closing %w: %v", err, closeErr)
}
}()
// …
When you’re writing something trivial/pure, Go’s error handling is fine, if maybe a tad bit verbose, but it quickly becomes nightmarish when you start to do nontrivial things that are typical for systems programming.FWIW I love Go, it’s my daily driver for most things. I still think it can get messy far too quickly
Zig has try syntax that expands to `expr catch |e| return e`, neatly solving a common use case of returning an error to the caller.
How well does it work in practice?
And even then this is just the same as the '?' operator Rust uses, which is mentioned in the post.
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.
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("invalid integer: %q", b)
}
fmt.Println("result:", x + y)
return nil
}
It's not adding anything that the Atoi function couldn't have reported. That's a perfect case for blindly passing an error up the stack. if err != nil { return nil, err; }
as a well-formatted line of go? Make it a special case somehow.My only big problem with if err != nil is that it eats up 3 lines minimum. If we could squish it down to 1, I'd be much more content.
All of that aside, I've come to learn that passing errors up the call stack without any wrapping or handling is a code smell. It is less than useless for me to attempt setting the value of cell A1 in an Excel sheet to "Foo" and then receive an out-of-range error because the developer made no attempt to even inform me of the context around the error. Let alone handling the error and attempting to correct state before giving up.
In my Excel example, the cause of the error was a data validation problem a couple columns over (F or so). The error was legitimately useless in troubleshooting.
(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.)
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++.
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.
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
Snip. No syntax for error handling is OK with me. Spelling out "hey I actually do need a stack trace" at every call site isn't.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.
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.
Fixing the problem purely from a syntactic perspective avoids any unexpected semantic changes that could lead to irreconcilable disagreements, as clearly demonstrated by the infamous try proposal. Very simple syntactical changes that maps clearly to the original error handling has the advantage of being trivial to implement while also avoids having the developers needing to learn something new.
And FWIW, my hatred of go error handling has not diminished with increased usage.
You mean "verbose error handling". All other proposals are also explicit, just not as verbose.
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.
x = foo() ||| <error case>
<happy case>
(With the specific symbol used in lieu of ||| to be decided)That is shorter and keeps the happy path unindented, even if it has additional such constructs, for example
x = foo() ||| return Error(err, “foo failed”)
y = bar() ||| return Error(err, “bar failed”)And so any change to any existing functionality is a breaking change that invalidates all code which uses that functionality? That design rule smells like hubris on the part of Go's designers. It only works if all future changes are extensions and never amendments.
They need to add/fix like 5-6 different parts of the language to even begin addressing this in a meaningful way.
https://gauntletlang.gitbook.io/docs/advanced-features/try-s...
func (r Result[T, E]) AndThen[OtherT any](func(T) Result[OtherT, E]) Result[OtherT, E] { ... }
which would enable error handling like sum := 0
parseAndAdd := func(s string) (func(string)Result[int, error]) { /* returns func which parses and adds to sum */ }
return parseAndAdd(a)().AndThen(parseAndAdd(b))
There's a reason why every other language is converging to that sort of functional setup, as it opens up possibilities such as try-transform generics for ranges.On the flip side, you can't have exception breakpoints in Go.
"it's been like this for this long now"
and "no one could ever agree to something"
leads to this amount of verbosity. Any of these keywords approach, `try`, `check`, or `?` would have been a good addition, if they kept the opportunity to also handle it verbosely.The argument that LLM now auto-completes code faster than ever is an interesting one, but I'm baffled by such an experienced team making this an argument, since code is read so many more times than it is written; they clearly identify the issue of the visual complexity while hand-waving the problem that it's not an issue since LLM are present to write it - it completely disregards the fact that the code is read many more times that it is written.
Visual complexity and visual rhythms are important factors in a programming language design, I feel. Go is excruciatingly annoying to read, compared to Dart, C, or any cleaner language.
Ultimately, it's just one of those "meh, too hard, won't do" solution to the problem. They'll write another one of those posts in 5 years and continue on. Clearly these people are not there to solve user problems. Hiding behind a community for decision making is weak. Maybe they should write some real world applications themselves, which involves having these error checks every 2nd lines.
At this point I wouldn't be upset if someone forked Go, called it Go++ and fixed the silly parts.
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.
But I can't stand the verbosity of the error handling. It drives me nuts. I also can't stand the level of rationalising that goes on when anyone dares to point out that Go's error handling is (obviously) verbose. The community has a pigheaded attitude towards criticism.
It also grinds my gears, because I really like that Go is in most other ways, simple and low on boilerplate. It's not unusual to see functions that are 50% error handling where the error handling actually DOES NOTHING. That's just insane.
I'm not super strongly against the constant error checking - I actually think it's good for code health to accept it as inherent complexity - but I do think some minor ergonomics would have been nice.
But in any case, why so much fear of being wrong?
> we have fine-grained control over the language version via go.mod files and file-specific directives
And that may be the real truth of it: Error handling in Go just isn't ... that much of a problem to force action?
I you are right that this is the truth of it. Error handling just isn’t that big a problem. 13% reported it on the survey cited. That doesn’t seem that significant. And honestly, after writing Go, I barely notice error handling as I’m writing/reading code anyway. If anything I appreciate it a bit more than exceptions.
Always something that can be complained about. But it doesn’t mean every complaint is a huge deal.
It seems that now that Ian's left the rest of the Go team is just being honest about what they are willing to spend their time on.
And I'm more than fine with that, because look at this comment section. You can't please everybody.
I dream if err, if err dreams me.
But I do loath the littered if err != nil { return err }
maybe a better autocomplete in the IDE/editor is a good compromise.
Why debacle?
This could be used to solve both "syntactic support for error handling", and also various other common complaints (such as lack of first class enums), without "polluting" the core language – they'd be optional packages which nobody would have to use if they don't want to
Of course, if one of these optional packages ever became truly prevalent, you could promote it to the standard library... but that would involve far less bikeshedding, because it would be about making the de facto standard de jure... and arguably, letting people vote with their feet is a much more reliable way of establishing consensus than any online discussion could ever be
Haskell solves this with the do notation, but the price is understanding monads. Go also aims to be easy to understand.
> We didn’t have a better solution at that time and didn’t pursue syntax changes for error handling for several years.
3 is small, and it seems like there were more years of not trying at all.
And the example of one proposal is telling. It had a major control flow issue, so got rejected, but then there is a recent proposal fixing those issues, and the post just goes on without further elaboration?
> 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.
How is this unfortunate? If the tweaks are good, update the proposal using them, so that's very fortunate? If they aren't, explain why there rejected? Or do you only expect a binary yes/no on the proposal as originally published?
I mean, no wonder with such an approach the only way forward is to give up.
func printSum(a, b string) error {
x := strconv.Atoi(a) or {
return error
}
y := strconv.Atoi(b) or {
return error
}
fmt.Println("result:", x + y)
return nil
}
or something along these lines...> 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.
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.perhaps it's a good thing that error handling is so explicit, and treated as a regular code path.
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).
> 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
Okay, so surely some syntactic sugar could make it more pleasant than the
if (err != nil) {
return nil, err
}
repeated boilerplate. Like, if that return is a tagged union you could do some kind of pattern matching?... oh, Go doesn't have sum-types. Or pattern matching.
Could you at least do some kind of generic error handler so I can call
y := Handle(MyFuncThatCanReturnError(x))
?... Okay, GoLang does not have tuples. Multiple returns must be handled separately.
Okay could I write some kind of higher-order function that handles it in a generic way? Like
y := InvokeWithErrorHandler(MyFuncThatCanReturnError, x)
?No? That's not an option either?
... why do you all do this to yourselves?
Error handling in Go is not just writing "if err != nil { return nil, err }" for every line. You are supposed to enrich the error to add more context to it. For example:
result, err := addTwoNumbers(a, b)
if err != nil {
return fmt.Errorf("addTwoNumbers(%d, %d) = %v", a, b, err)
}
This way you can enrich the error message and say what was passed to the function. If you try to abstract this logic with a "Handle" function, you'll just create a mess. You'll save yourself the time of writing an IF statement, but you'll need a bunch of arguments that will just make it harder to use.Not to mention, those helper functions don't account for cases where you don't just want to bubble up an error. What if you want to do more things, like log, emit metrics, clean up resources, and so on? How do you deal with that with the "Handle()" function?
Resist enshittification, the greatest advantage in foundational software is sometimes saying no to new features.
$ find . -name "*.go" | xargs grep 'if err !=' | wc -l
242
$ find . -name "*.go" | xargs grep 'if err ==' | wc -l
12
So about 5% of the error checking code is about handling the edge cases, where we are very much interested in what the error actually is, and need to handle those conditions carefully.If you discard that as "error handling noise", you're in for a bug. Which is, by the way, perhaps the worst side-effect of verbose, repetitive error handling.
Apropos syntax highlighting: many themes in regular use (certainly most of the defaults) choose a low-contrast color for the comments. The comments are often the most important part of the code.
> The goal of the proposal process is to reach general consensus about the outcome in a timely manner. If proposal review cannot identify a general consensus in the discussion of the issue on the issue tracker, the usual result is that the proposal is declined.
> None of the error handling proposals reached anything close to a consensus, so they were all declined.
> Should we proceed at all? We think not.
The disconnect here is of course that everyone has opinions and Google being design-by-committee can’t make progress on user-visible changes. Leaving the verbose error handling is not the end of the world, but there’s something here missing in the process. Don’t get me wrong, I love inaction as a default decision, but sometimes a decision is better than nothing. It reminds me of a groups when you can’t decide what to have for dinner – the best course of action isn’t to not eat at all, it’s to accept that everyone won’t be happy all the time, and take ownership of that unhappiness, if necessary, during the brief period of time when some people are upset.
I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go. IME these stylistic preferences are as easily abandoned as they are acquired.
To put another way: imagine if gofmt was launched today. It would be absolutely impossible to release through a consensus based process. Just tabs vs spaces would be 100 pages on the issue tracker of people willing to die on that hill. Yet, how many people complain about gofmt now that it’s already there? Even the biggest bike shedders enjoy it.
Everyone feels equipped to have an opinion about "what should be the syntax for an obvious bit of semantics." There's no expertise required to form such an opinion. And so there are as many opinions on offer as there are Go developers to give them.
Limit input on the subject to just e.g. the people capable of implementing the feature into the Go compiler, though, and a consensus would be reached quickly. Unlike drive-by opinion-havers, the language maintainers — people who have to actually continue to work with one-another (i.e. negotiate in an indefinite iterated prisoner's dilemma about how other language minutiae will work), are much more willing to give ground "this time" to just move on and get it working.
(Tangent: this "giving ground to get ground later" is commonly called "horse trading", but IMHO that paints it in a too-negative light. Horse trading is often the only reason anything gets done at all!)
The thing is, inaction is not simply "not taking an action"; Inaction is taking active action of accepting the current solution.
> I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go.
But people may leave go if they constantly avoid fixing any of problems with the language. The more time passes, the more unhappy people become with the language. It will be a death by a thousand cuts.
I love go. But their constant denial do fix obvious problems is tiring.
Invalid comparison - eating one foodstuff or another affects a few people for a few hours. Significantly changing a popular language affects every single user of it forever.
Not in this case. The most popular Go proposal/issue of all times was 'leave "if err != nil" alone': https://github.com/golang/go/issues?q=is%3Aissue%20%20sort%3...
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".
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.
Go chose not to change the error handling - Nature remained in balance.
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.
According to 13% of respondents. So yes, it's the "#1 issue", but also not by a huge overwhelming majority or anything.
- 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.
"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;"
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."
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.
But now a sort of democracy is required for changes. I’m not sure this is necessary.
Newcomers often push back on this aspect of the language (among other things), but in my experience, that usually fades as they get more familiar with Go’s philosophy and design choices.
As for the Go team’s decision process, I think it’s a good thing that the lack of consensus over a long period and many attempts can prompt them to formally define a position.
I have many things to complain about for other languages that I’m sure are top-tier complaints too
I wouldn’t be surprised that when the pro-exception-handling crowd eventually wins, it will lead to hard forks and severe fragmentation of the entire ecosystem.
Edit: looking at the results of their H1 2024 survey, they're making a hard turn into AI, and most likely will be developing AI libraries to close the gap with Python.
Don't expect language features.
>The Go team takes community feedback seriously
It feels like reading satire, but it's real.
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
Dudes, error handling is THE worst part of Go. All of it.
And not just the lack of the "?" operator. It's also the lack of stacktraces, the footguns with nils-that-are-not-nil, problems with wrapping, the leakage of internal error messages to users (without i18n).
Literally everything. And you're just shutting down even _discussions_ of this?
I am not saying that the mechanism is perfect but it is more useful if we have it than not. IMO it's only weakness is that you never know if a new exception type is thrown by a nested function. This is a weakness for which we really don't have a solid solution - Java tried this with checked exceptions.
Go not using such a paradigm to me is bonkers. Practically every language has such a construct and how we use it best is pretty much convention these days.
I regularly run into internal compiler errors these days for pretty normal looking code.
It's getting to the point where I'm reluctant to invest more time in the language right now.
UPDATE: See comment below for full error message and a link to the code.
What error are you talking about?
> For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling