The Art of SQL Query Optimization (jnidzwetzki.github.io)
2 points by gwen-shapira 6m ago 0 comments
ChatGPT and Public MCP Servers (community.openai.com)
1 points by johannes_aready 25m ago 1 comments
(On | No) Syntactic Support for Error Handling
372 henrikhorluck 512 6/3/2025, 4:18:45 PM 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.
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.
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.
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).
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.
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.
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.
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/
[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.
> That's a separate question from asking why people think that it wasn't considered. An interesting one though. To an experienced Haskell programmer, it would be worth asking why not take the leap and make it easy to express monads naturally. Solving the error handling case elegantly would just be one side effect that you get out of it. There are many other benefits, but I don't want to make this into a Haskell tutorial.
Hmm, but you could say that for any idea that sounds good. Why not add a borrow checker into Go while we're at it, and GADTs, and...
Being blunt, this is just incorrect framing. Concepts like monads and do notation are not inherently "good" or "bad", and neither is a language feature like a borrow checker (which also does not mean you won't miss it when it's not there in languages like Go, either). Out of context, you can't judge whether it's a good idea or not. In context, we're talking about the Go programming language, which is not a blank slate for programming language design, it's a pre-existing language with extremely different values from Haskell. It has a pre-existing ecosystem built on this. Go prioritizes simplicity of the language and pragmatism over expressiveness and rich features nearly every time. This is not everyone's favorite tradeoff, but also, programming language design is not a popularity contest, nor is it an endeavor of mathematical elegance. Designers have goals, often of practical interest, that require trade-offs that by definition not everyone will like. You can't just pretend this constraint doesn't exist or isn't important. (And yes we know, Rob Pike said once in 2012 that Go was for idiots that can't understand a brilliant language. If anyone is coming here to make sure to reply that under each comment as usual on HN, consider it pre-empted.)
So to answer the question, would it be worth the leap to make it easy to express monads naturally in Go? Obviously, this is a matter of opinion and not fact, but I think this is well beyond the threshold where there is room for ambiguity: No. It just does not mesh with it at all, does not match nearly any other decision made anywhere else with regards to syntax and language features, and just generally would feel utterly out of place.
A less general version of this question might be, "OK: how about just sum types and go from there?"—you could probably add sum types and express stuff like Maybe/Either/etc. and add language constructs on top of this, but even that would be a pretty extreme departure and basically constitute a totally new, distinct programming language. Personally, I think there's only one way to look at this: either Go should've had this and the language is basically doomed to always have this flaw, or there is room in the space of programming languages for a language that doesn't do this without being strictly worse than languages that do (and I'm thinking here in terms of not just elegance or expressiveness but of the second, third, forth, fifth... order effects of such a language design choice, which become increasingly counter-intuitive as you follow the chain.)
And after all, doesn't this have to be the case? If Haskell is the correct language design, then we already have it and would be better off writing Haskell code and working on the GHC. This is not a sarcastic remark: I don't rule out such dramatic possibilities that some programming languages might just wind up being "right" and win out in the long term. That said, if the winner is going to be Haskell or a derivative of it, I can only imagine it will be a while before that future comes to fruition. A long while...
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.
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.
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.But of course that would hurt them and the community in so many levels that they don't want to admit...
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...
- 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.
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.
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…
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.
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.
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.
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.
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.
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.
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.
Where is this evidence? Where is the data that I am supposed to believe?
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.
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.
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.
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.
The public/private stuff is mostly useful for publishing modules with sound APIs.
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.
"&()".
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.
`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.
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.
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.
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?
Did Rust become a clusterfuck like C++?
Is Go as timeless as it was during release?
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.
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.
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.Works especially well in languages that can make assignments in if statements, e.g:
if foo = 42 { }
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.
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.
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.
https://news.ycombinator.com/item?id=36398874
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.
- 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 :)
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.
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?
Which target metrics do you consider to be good despite Goodhart's law?
https://github.com/dave/courtney
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.
* 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.
Then we're back to having stack frames for framework and runtime code in our error traces.
Or perhaps the people who wrote the Go standard library don't follow the ideal Go best practices?
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.
My guess is they will still write Go even if error handling stays the same forever.
Common Lisp actually does cool things (if a bit niche) things with MRVs, they're side-channels through which you can obtain additional information if you need it e.g. every common lisp rounding functions returns rounded value... and the remainder as an extra value.
So if you call
you get 2, but if you you get 2 and 1.r, err := f()
r := f()
_, err := f()
The other two are completely routine and will work just fine with lists in JS or tuples in Python or Rust (barring a few syntactic alterations).
You can pass multiple return values of a function as parameters to another function if they fit the signature.
for example:
this can be used in cases such as control loops, to centralize error handling for multiple separate functions, instead of writing out the error handling separately for each function.That said, there are libraries out there that implement Result as generic type and it's fine working with them, as well.
I don't see what the hubbub is all about.
Perhaps what you are really trying to say is that multiple function arguments is insane full stop. You can pass in an array/tuple to the single input just the same. But pretty much every language has settled on them these days – so it would be utterly bizarre to not support them both in and out. We may not have known any better in the C days, but multiple input arguments with only one output argument is plain crazy in a modern language. You can't even write an identity function.
(this is what zig does)
Where multiple input arguments are present, not having multiple output arguments is just strange.
https://ziglang.org/documentation/0.14.1/#call
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'
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.
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
In fact the lack of sum types seems to be why you need everything to have a zero value in the first place: because sums are products you need a nonsensical value when the sum is the other type.
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).
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:
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.
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.
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.
A thread from two days ago bemoans this point:
https://news.ycombinator.com/item?id=44149809
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
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)
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.
No comments yet
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.
- It isn't easily breakpointable.
- It favors "bubbling up" as-is over enriching or handling.
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
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.
For example instead of
they could have something like this: which desugars to: 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?
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.
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.
Go is very readable in my experience. I'd like to keep it that way.
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.
So it's:
Rely on the programmer to identify that an error was made and in 95% of situations just bubble it up, do something useful 5% of the time, but "deal with" the error 100% of the time OR
Rely on the programmer to identify the 5% of situations where they don't want the error to just bubble up and add special handling for that case specifically.
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.
It makes them almost as informative as languages with stack traces! Imagine if Go had a syntax like python's
I'd love that.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.
FWIW I love Go, it’s my daily driver for most things. I still think it can get messy far too quickly
In fact it’s quite common to “commit” on close, at least from what I’ve seen.
The language’s status quo forces everyone to think about errors more deeply than in other languages and acknowledges that the error case is as critical and worthy of the programmer’s attention.
Not really. Rust also forces you think deeply about errors but don't bother you with verbose syntax. I think Swift was also similar.
gofmt is the good bit about working in Go. Pretty much everybody uses it, and so you can use it too. Some other languages have similar tools, but they're not as pervasive, so it's far too easy to end up in a situation where you can't use the tool because it would just make too much of a mess of the inconsistently manually-formatted stuff that's already there.
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.
But I do loath the littered if err != nil { return err }
maybe a better autocomplete in the IDE/editor is a good compromise.
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.
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.
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.
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.
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.
On the flip side, you can't have exception breakpoints in Go.
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.
Anyway you can always copy paste it in the normal linear format.
That is shorter and keeps the happy path unindented, even if it has additional such constructs, for example
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.
They need to add/fix like 5-6 different parts of the language to even begin addressing this in a meaningful way.
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.
https://gauntletlang.gitbook.io/docs/advanced-features/try-s...
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.
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
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.
Why debacle?
Haskell solves this with the do notation, but the price is understanding monads. Go also aims to be easy to understand.
I dream if err, if err dreams me.
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.
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:
Zero values are a fundamental, non-optional, "feature" of Go.
> But if you’d rather force an explicit value to be supplied, what’s the harm?
What happens if you use the function above with your type?
Or reflect.Zero?
> 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.perhaps it's a good thing that error handling is so explicit, and treated as a regular code path.
[1]: https://news.ycombinator.com/item?id=44135638
(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++.
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.
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?
If it was going to be killed by this approach, it would now be dead.
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.
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
Resist enshittification, the greatest advantage in foundational software is sometimes saying no to new features.
> 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.
For many people the current Go error handling just isn't a problem. Some even prefer it over the overengineered solutions in other languages. This brutalist simplicity is a core design feature I personally enjoy the most with Go, and forcing me to use some syntax sugar or new keywords would make it less enjoyable to use. I also don't think that generics were a net benefit, but at least I'm not forced to use them.
Go is a cemented language at this point, warts and all. People who don't like it likely won't come around if the issues they're so vocal about were fixed. It's not like there's a shortage of languages to choose from.
Python does offer a lot more utility for the expert these days, but it also went from the maxim of "There is one obvious way to do it" to having 5-6 ways to format strings, adding more things to be familiar with, causing refactoring churn as people chase the latest way to do it, etc.
I'm a C++ developer. I wouldn't want to go back to older versions of the language, but it's also very hard to recruit any newer programmers into using it willingly, and the sheer amount of stuff in it that exists concurrently is a big reason why.
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...
I suspect it is the latter.
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.
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.
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.
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.
"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."
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.
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 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.
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
Okay, so surely some syntactic sugar could make it more pleasant than the
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
?... 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
?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:
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?
You can easily imagine
or Or any other common error-handling pattern.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.
Of course you may have been joking, in which case “haha”. xD
I have many things to complain about for other languages that I’m sure are top-tier complaints too
But on the other hand, people who are "used to the way things are" are often the worst people to evaluate whether changes are beneficial. It seems like the new people are the ones that should be listened to most carefully.
I'm not saying the Go team was wrong in this decision, just that your heuristic isn't necessarily a good one.
To me, it makes sense for the Go team to focus on improving Go for the vast majority of its users over the opinions of people who don't like it that much in the first place. There's millions of lines of code written in Go and those are going to have to be maintained for many years. Of utmost priority in my mind is making Go code more correct (i.e. By adding tools that can make code more correct-by-construction or eliminate classes of errors. I didn't say concurrency safety, but... some form of correctness checking for code involving mutexes would be really nice, something like gVisor checklocks but better.)
And on that note, if I could pick something to prioritize to add to Go, it would probably be sum types with pattern matching. I don't think it is extremely likely that we will see those, since it's a massive language change that isn't exactly easy to reconcile with what's already here. (e.g. a `Result` type would naturally emerge from the existence of sum types. Maybe that's an improvement, but boy that is a non-trivial change.)
Bad auto-correct, my bad
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.
In a quick search, you seem to be one of them: https://news.ycombinator.com/item?id=41136266 https://news.ycombinator.com/item?id=40855396
You don't see me going around $languages_I_dislike threads slagging off the language, much less demanding features. Not saying anything is an option you know.
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.
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?
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
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
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!
No comments yet