If you feel the need (as many have in this thread) to breezily propose something the Go Team could have done instead, I urge you to click the link in the article to the wiki page for this:
https://go.dev/wiki/Go2ErrorHandlingFeedback
or the GitHub issue search: https://github.com/golang/go/issues?q=+is%3Aissue+label%3Aer...
I promise that you are not the first to propose whatever you're proposing, and often it was considered in great depth. I appreciate this honest approach from the Go Team and I continue to enjoy using Go every day at work.
The draft design document that all of the feedback is based on mentions C++, Rust, and Swift. In the extensive feedback document you link above I could not find mention of do-notation/for-comprehensions/monadic-let as used Haskell/Scala/OCaml. I didn't find anything like that in the first few pages of the most commented GitHub issues.
You make it out like the Go Team are programming language design wizards and people here are breezily proposing solutions that they must have considered but lets not forget that the Go team made the same blunder made by Java (static typing with no parametric polymorphism) which lies at the root of this error handling problem, to which they are throwing up their hands and not fixing.
I think Go should have shipped with generics from day one as well.
But you breezily claiming they made the same blunder as Java omits the fact that they didn't make the same blunder as Rust and Swift and end up with nightmarish compile times because of their type system.
Almost every language feature has difficult trade-offs. They considered iteration time a priority one feature and designed the language as such. It's very easy for someone looking at a language on paper to undervalue that feature but when you sit down and talk to users or watch them work, you realize that a fast feedback loop makes them more productive than almost any brilliant type system feature you can imagine.
This is a very good point, fast compilation times are a huge benefit. The slow compiler is a downside of languages like Rust, Scala, and Haskell. Especially if you have many millions of lines of code to compile like Google.
However, OCaml has a very fast compiler, comparable in speed to Go. So a more expressive type system is not necessarily leading to long compilation times.
Furthermore, Scala and Haskell incremental type checking is faster than full compilation and fast enough for interactive use. I would love to see some evidence that Golang devs are actually more productive than Scala or Haskell devs. So many variables probably influence dev productivity and controlling for them while doing a sufficiently powered experiment is very expensive.
Take a look a the kubernetes source code. It's millions of lines, and almost all of it is generated. In a language like C++ or Rust, the vast majority of it would be template or macro instantiations.
For an apples-to-apples comparison of compilation speed, you should either include the time it takes go generate to run, and the IDE to re-index all the crap it emits, or you should count the number of lines of code in the largest intermediate representation that C++ or Rust has.
> For an apples-to-apples comparison of compilation speed, you should either include the time it takes go generate to run
But that would be unfair to the very design choice of omitting metaprogramming while exposing the go/ast library to users to foster code generation.
What makes you think Rust’s compile times are related to its type system?
The way the type system interacts with the rest of the language leads you down the path to monomorphization as the compilation strategy. Monomorphizing is what gives you huge piles of instantiated code that then has to be run through the compiler back end.
Blaming it on LLVM like another comment does misses the point. Any back end is slow if you throw a truck-load of code at it.
I'm not saying monomorphization is intrinsically bad. (My current hobby language works this way.) But it's certainly a trade-off with real costs and the Go folks didn't want their users to have to pay those costs.
Monomorphization has got nothing to do with type system though. If you have a GC (as go does), you can automatically box your references and go from a `impl Trait` to a `&mut dyn Trait` with the GC taking care of value vs reference semantics. Monomorphization is orthogonal to how you define the set of valid arguments.
Except if your traits are not dyn-compatible. Which I believe a lot of Rust's traits are not. That restriction is specifically why Go does not allow methods to have extra type parameters: To make it possible for the language implementation to choose its own tradeoff between monomorphization and boxing.
So I don't think you can say that this has nothing to do with the type system. Here is a restriction in the Go type system that was specifically introduced to allow a broad range of implementation choices. To avoid being forced to choose slow compilers or slow code: https://research.swtch.com/generic
The Go type system and the way it does generics is directly designed to allow fast compile times.
> you can automatically box your references
Yes, but that is now a different runtime cost which Go also didn't want to pay.
The language goes to great pains to give you pretty good control over layout in memory and avoid the "spray of tiny objects on the heap with pointers between them" that you get in Java and most other managed languages.
I think Swift maybe does something more clever with witness tables, but I don't recally exactly how it works.
It's not an easy problem.
You realize that having a generics and having monomorphization are two orthogonal things, right?
If you're not aiming for the highest possible performance, you can type erase your generics and avoid the monomorphization bloat. Rust couldn't because they wanted to compete with C++, but Go definitely could have.
Last time I checked, Rust's slow compile times were due to LLVM. In fact, if you want to make Rust faster to compile, you can compile it to wasm using cranelift.
Not just LLVM in itself but the Front-end codegen: AFAIK the rust front-end emits way too much LLVM IR and then counts on LLVM to optimize and they have been slowly adding optimizations inside the front-end itself to avoid IR bloat.
And there's also the proc macro story (almost every project must compile proc_macro2 quote and syn before the actual project compilation even starts).
Thanks for the clarification.
This has been a lazy excuse/talking point from the Go team for a while, but in realitiy Generics aren't the reason why Rust and Swift compile slowly, as can be easily shown by running cargo check on a project using a hefty dose of generics but without procedural macros.
> lets not forget that the Go team made the same blunder made by Java
To be fair, they were working on parametric polymorphism since the beginning. There are countless public proposals, and many more that never made it beyond the walls of Google.
Problem was that they struggled to find a design that didn't make the same blunder as Java. I'm sure it would have been easy to add Java-style generics early on, but... yikes. Even the Java team themselves warned the Go team to not make that mistake.
At least Java supports covariance and contravariance where Go only supports invariant generics.
Java has evolved to contain much of “ML the good parts” such as that languages like Kotlin or Scala that offer a chance to be just a bit better in the JVM look less necessary
> Java has evolved to contain much of “ML the good parts”
Can you give some examples?Not OP. IMO the recent Java changes, including pattern matching (especially when using along with sealed interface), virtual threads (and structured concurrency on the way), string templates, are all very solid additions to the language.
Using these new features one can write very expressive modern code while still being interoperable with the Java 8 dependency someone at their company wrote 20 years ago.
fyi: string templates were just a preview and removed in Java 23
For Java systems that I work on for my own account there is a lot of stuffing things like SQL queries into resource files so that I don't have to mess around with quotes and such.
Like a lot of other languages, Java has gotten a big dose of
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.
> blunder made by Java
For normies, what is wrong with Java generics? (Do the same complaints apply to C# generics?) I came from C++ to Java, and I found Java generics pretty easy to use. I'm not interested in what "PL (programming language) people" have to say about it. They dislike all generic/parametric polymorphism implementations except their pet language that no one uses. I'm interested in practical things that work and are easy for normies to learn and use well. > Even the Java team themselves warned the Go team to not make that mistake.
Do you have a source for that?> I'm not interested in what "PL (programming language) people" have to say about it. They dislike all generic/parametric polymorphism implementations except their pet language that no one uses.
That's strange. I seem to recall the PL community invented the generics system for Java [0,1]. Actually, I'm pretty sure Philip Wadler had to show them how to work out contravariance correctly. And topically to this thread, Rob Pike asked for his help again designing the generics system for Go [2,3]. A number of mistakes under consideration were curtailed as a result, detailed in that LWN article.
There are countless other examples, so can you elaborate on what you're talking about? Because essentially all meaningful progress on programming languages (yes, including the ones you use) was achieved, or at least fundamentally enabled, by "PL people".
[0] https://homepages.inf.ed.ac.uk/wadler/gj/
[1] https://homepages.inf.ed.ac.uk/wadler/gj/Documents/gj-oopsla...
Yeah, it _doesn’t_ apply to C# generics. Basically, if you’ve got List<Person> and List<Company> in C#, those are different classes. In Java, there’s only one class that’s polymorphic. This causes a surprising number of restrictions: https://docs.oracle.com/javase/tutorial/java/generics/restri...
Type erasure is what is wrong with Java generics. It causes many issues downstream.
> It causes many issues downstream.
I don't understand this part. Can you give some concrete examples? In my experience, Google Gson and Jackson FasterXML can solve 99.9% of the Java Generic issues that I might have around de/ser.I could, or you could use google. Neither of those tools can solve any issue caused by type erasure.
Just to give some examples, the instanceof operator does not work with generic types, it's not possible to instantiate a generic type (can't do a new T()), can't overload methods that differ only in generic parameter type (so List<String> vs List<Integer>) and so on. Some limitations can be worked around with sending around explicit type info (like also sending the Class<T> when using T), reflection etc., but it's cumbersome, and not everything can be solved that way.
Even Rust and F#[1] don't have (generalized) do notation, what makes it remotely relevant to a decidedly non-ML-esque language like Go?
[1] Okay fine, you can fake it with enough SRTPs, but Don Syme will come and burn your house down.
IDK, Python was fine grabbing list comprehensions from Haskell, yield and coroutines from, say, Modula-2, the walrus operator from, say, C, large swaths from Smalltalk, etc. It does not matter if the languages are related; what matters is whether you can make a feature / approach fit the rest of the language.
Generalized do notation as GP is proposing requires HKT. I don't think it's controversial to say that Go will not be getting HKT.
Afaik the F# supports do notation through computation expressions.
Like Rust, F# doesn't have higher-kinded types so it's not generalized like GP is proposing. Each type of computation expression is tied to a specific monad/applicative.
hahaha :D
It fascinates me that really smart and experienced people have written that page and debated approaches for many years, and yet nowhere on that page is the Haskell-solution mentioned, which is the Maybe and Either monads, including their do-notation using the bind operator. Sounds fancy, intimidating even, but is a very elegant and functionally pure way of just propagating an error to where it can be handled, at the same time ensuring it's not forgotten.
This is so entrenched into everybody writing Haskell code, that I really can't comprehend why that was not considered. Surely there must be somebody in the Go community knowing about it and perhaps appreciating it as well? Even if we leave out everybody too intimidated by the supposed academic-ness of Haskell and even avoiding any religios arguments.
I really appreciate the link to this page, and overall its existence, but this really leaves me confused how people caring so much about their language can skip over such well-established solutions.
I don't get why people keep thinking it was forgotten; I will just charitably assume that people saying this just don't have much background on the Go programming language. The reason why is because implementing that in any reasonable fashion would require massive changes to the language. For example, you can't build Either/Maybe in Go (well, of course you can with some hackiness, but it won't really achieve the same thing) in the first place, and I doubt hacking it in as a magic type that does stuff that can't be done elsewhere is something the Go developers would want to do (any more than they already have to, anyway.)
Am I missing something? Is this really a good idea for a language that can't express monads naturally?
> I don't get why people keep thinking it was forgotten
Well, I replied to a post that gave a link to a document that supposedly exhaustively (?) listed all alternatives that were considered. Monads are not on that list. From that, it's easy to come to the conclusion that it was not considered, aka forgotten.
If it was not forgotten, then why is it not on the list?
> Is this really a good idea for a language that can't express monads naturally?
That's a separate question from asking why people think that it wasn't considered. An interesting one though. To an experienced Haskell programmer, it would be worth asking why not take the leap and make it easy to express monads naturally. Solving the error handling case elegantly would just be one side effect that you get out of it. There are many other benefits, but I don't want to make this into a Haskell tutorial.
It's not an exhaustive list of every possible way to handle errors, but it is definitely, IMO, roughly an exhaustive list of possible ways Go could reasonably add new error handling tools in the frame of what they already have. The reason why monads and do notation don't show up is because if you try to write such a proposal it very quickly becomes apparent that you couldn't really add it to the Go programming language without other, much bigger language change proposals (seriously, try it if you don't believe me.) And for what it's worth, I'm not saying they shouldn't, it's just that you're taking away the wrong thing; I am absolutely 100% certain this has come up (in fact I think it came up relatively early in one of the GitHub issues), but it hasn't survived into a proposal for a good reason. If you want this, I believe you can't start with error handling first; sum types would probably be a better place to start.
> That's a separate question from asking why people think that it wasn't considered. An interesting one though. To an experienced Haskell programmer, it would be worth asking why not take the leap and make it easy to express monads naturally. Solving the error handling case elegantly would just be one side effect that you get out of it. There are many other benefits, but I don't want to make this into a Haskell tutorial.
Hmm, but you could say that for any idea that sounds good. Why not add a borrow checker into Go while we're at it, and GADTs, and...
Being blunt, this is just incorrect framing. Concepts like monads and do notation are not inherently "good" or "bad", and neither is a language feature like a borrow checker (which also does not mean you won't miss it when it's not there in languages like Go, either). Out of context, you can't judge whether it's a good idea or not. In context, we're talking about the Go programming language, which is not a blank slate for programming language design, it's a pre-existing language with extremely different values from Haskell. It has a pre-existing ecosystem built on this. Go prioritizes simplicity of the language and pragmatism over expressiveness and rich features nearly every time. This is not everyone's favorite tradeoff, but also, programming language design is not a popularity contest, nor is it an endeavor of mathematical elegance. Designers have goals, often of practical interest, that require trade-offs that by definition not everyone will like. You can't just pretend this constraint doesn't exist or isn't important. (And yes we know, Rob Pike said once in 2012 that Go was for idiots that can't understand a brilliant language. If anyone is coming here to make sure to reply that under each comment as usual on HN, consider it pre-empted.)
So to answer the question, would it be worth the leap to make it easy to express monads naturally in Go? Obviously, this is a matter of opinion and not fact, but I think this is well beyond the threshold where there is room for ambiguity: No. It just does not mesh with it at all, does not match nearly any other decision made anywhere else with regards to syntax and language features, and just generally would feel utterly out of place.
A less general version of this question might be, "OK: how about just sum types and go from there?"—you could probably add sum types and express stuff like Maybe/Either/etc. and add language constructs on top of this, but even that would be a pretty extreme departure and basically constitute a totally new, distinct programming language. Personally, I think there's only one way to look at this: either Go should've had this and the language is basically doomed to always have this flaw, or there is room in the space of programming languages for a language that doesn't do this without being strictly worse than 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...
Well, Rust's `?` was initially designed as a hardcoded/poor man's `Either` monad. They quote `?` as being one of the proposals they consider, so I think that counts?
Source: I'm one of the people who designed it.
It was not forgotten. Maybe/Either and 'do-notation' are literally what Rust does with Option/Result and '?', and that is mentioned a lot.
That said as mentioned in a lot of places, changing errors to be sum types is not the approach they're looking for, since it would create a split between APIs across the ecosystem.
Where there’s a will there’s a way. Swift is almost universally compatible with objective-c and they are two entirely different languages no less. If an objective-c function has a trailing *error parameter, you can, in swift, call that function using try notation and catch and handle errors idiomatically. All it takes is for one pattern to be consistently expressible by another. Why can’t Result/Either types be api-compatible with functions that return tuples?
As I mentioned, it's the same method Rust uses, and there are multiple reasons on the papers linked on why that is not desired.
I didn't say desired. It would work. Do it and if nobody uses it then so be it. Don’t balk and say “well we could but the elite minds in charge have high quibbles with how it would affect the feel of the language in this one absurd edge case, so we won’t”. Just special case the stupid pattern.
Indeed, I can testify that `?` was very much designed with the do-notation in mind.
> and yet nowhere on that page is the Haskell-solution mentioned
What do you mean? Much of the discussion around errors from above link is clearly based on the ideas of Haskell/monads. Did you foolishly search for "monad" and call it a day without actually reading it in full to reach this conclusion?
In fact, I would even suggest that the general consensus found there is that a monadic-like solution is the way forward, but it remains unclear how to make that make sense in Go without changing just about everything else about the language to go along with it. Thus the standstill we're at now.
Why not change everything along with it? It can only get better
We have! Several times, in fact. You might recognize those changes by the names Rust, Zig, etc.
But for those who can't, for whatever reason, update their code to work with the substantial language changes, they are interested to see if there is also a solution that otherwise fits into what they've already got in a backwards-compatible way.
It's probably already answered somewhere, but I am curious why it's such a problem in Go specifically, when nearly every language has something better - various different approaches ... is the problem just not being able to decide / please everyone, or there's something specific about Go the language that means everyone else's solutions don't work somehow?
> is the problem just not being able to decide / please everyone,
Reading this article? in fact yes(?):
> After so many years of trying, with three full-fledged proposals by the Go team and literally hundreds (!) of community proposals, most of them variations on a theme, all of which failed to attract sufficient (let alone overwhelming) support, the question we now face is: how to proceed? Should we proceed at all?
> We think not.
This is a problem of the go designers, in the sense that are not capable to accept the solutions that are viable because none are total to their ideals.
And never will find one.
____
I have use more than 20 langs and even try to build one and is correct that this is a real unsolved problem, where your best option is to pick one way and accept that it will optimize for some cases at huge cost when you divert.
But is know that the current way of Go (that is a insignificant improvement over the C way) sucks and ANY of the other ways are truly better (to the point that I think go is the only lunatic in town that take this path!), but none will be perfect for all the scenarios.
> But is know that the current way of Go (that is a insignificant improvement over the C way) sucks and ANY of the other ways are truly better […]
This is a bold statement for something so subjective. I'll note that the proposal to leave the status quo as-is is probably one of the most favorably voted Go proposals of all time: https://github.com/golang/go/issues/32825
Go language design is not a popularity contest or democracy (if nothing else because it is not clear who would get a vote). But you won't find any other proposal with thousands of emoji votes, 90% of which are in favor.
I get the criticism and I agree with it to a degree. But boldly stating that criticism as objective and universal is uninformed.
This issue contradicts the cited surveys where error handling is identified as the most important issue. Isn't the survey more reliable way to read the community room than a github issue?
I understand that the decision could be correct for the situation (ie: if the stated goal is have a proposal with enough support and it was not reached not proceed is correct), that is different that the handling of error as-is is bad (that is the reason the people spend years to solve it)
Go has specific goals like not hiding control flow. This would go against those goals, at least the ways people have thought to do it so far.
Isn't defer hidden control flow? The defer handling can happen at any point in the function, depending on when errors happen. Exactly like a finally block.
I don't see how Try (the ? operator) is hidden control flow. It's terse, but it's not hidden.
Even the laziest programmer is going to want to wrap the error with another error, and usually you will want to do more than that.
You can put that in-band, with something like:
v := funcWithError()? err := {
return fmt.Errorf("lazy programmer wrapping: %w", err)
}
But in that case what have you really gained?Or you can do some kind of magic to allow it to happen out of band:
// found somewhere else in the codebase
func wrapErr(err error) error {
if err == nil {
return nil
}
return fmt.Errorf("lazy programmer wrapping: %w", err)
}
v := funcWithError()?(wrapErr)
But that's where you start to see things hidden.I personally agree, but I’m not the go team. The hidden control flow was specifically called out but about the try keyword. I like the ? and similar ways of checking nulls, but personally I don’t mind the verbosity in go, even if there are footguns.
IMO: because it behaves like structured control flow (i.e. there is a branch) but it doesn't look like structured control flow (i.e. it doesn't look like there is a branch; no curly braces). I don't think there's a single other case in the Go programming language: it doesn't even have the conditional ternary operator, for example.
`return` doesn't have braces either.
Closest thing to a real interblock branch without braces, IMO, is `break` and `continue`, but those are both at least lone statements, not expressions. It "looks like" control flow. Personally, I don't count `return`, I view it as it's own thing from a logical standpoint. Obviously if we were talking about literal CPU doing a jump, well then a lot of things would count, but that's not what I mean in the frame of structured control flow and more in the realm of implementation details.
I could have sworn that like most modern languages Go has `break label` although being statement oriented it doesn't have `break label value`.
It does. Hell, Go also has a goto statement as well, although obviously that's unstructured control flow.
A more refined version of what I originally said would say "conditional branch" instead of "branch", but I'll admit that my original message should have been worded more carefully. I think people understood it, but taken literally it's not a strong argument.
I love Rust's ability to return a value from a loop, it's so nice.
the obvious solution is try-catch, Java style. Which I'm surprised it's not even mentioned in the article. Not even when listing cons that wouldn't have been there with try-catch.
But of course that would hurt them and the community in so many levels that they don't want to admit...
I strongly do not like try/catch. Just to list the limitations of exceptions that come to mind,
- try/catch exceptions obscure what things can throw errors. Just looking at a function body, you can't see what parts of the functions could throw errors.
- Relatedly, try/catch exceptions can unwind multiple stack frames at once, sometimes creating tricky, obscure control flow. Stack unwinding can be useful, especially if you really do want to traverse an arbitrary number of stack frames (e.g. to pass an error up in a parser or interpreter, or for error cases you really don't want to think about handling as part of the normal code flow) but it's tricky enough that it's undesirable for ordinary error handling.
- I think most errors, like I/O errors, are fairly normal occurrences, i.e. all code should be written with handling I/O errors in mind; this is not a good use case for this type of error handling mechanism—you might want to pass the error up the stack, but it's useful to be confronted with that decision each time! With exceptions, it might be hard to even know whether a given function call might throw an I/O error. Function calls that are fallible are not distinguishable from function calls that are infallible.
- This is also a downside of Go's current error handling; with try/catch exceptions you can't usually tell what exceptions a function could throw. (Java has checked exceptions, but everyone hates them. The same problem doesn't happen for enum error types in Rust Result, people generally like this.)
(...But that's certainly not all.)
Speaking just in terms of language design, I feel that Rust Result, C++ std::expected, etc. are all going in the right direction. Even Go just having errors be regular values is still better in my opinion.
(Still, traditional exceptions have been proposed too, of course, but it wasn't a mistake to not have exceptions in Go, it was intentional.)
> but it wasn't a mistake to not have exceptions in Go, it was intentional.
It does have them, though, and always has. Use is even found in the standard library (e.g. encoding/json). They are just not commonly used for this because of the inherit problems with using them in this way as you have already mentioned. But you can. It is a perfectly valid approach where the tradeoffs are acceptable.
But, who knows what the future holds? Ruby in the early days also held the same preference for error values over exceptions... Until Ruby on Rails came along and shifted the prevailing attitude. Perhaps Go will someday have its "Ruby on Rails" moment too.
Disagree. We could argue what counts as "exceptions", the jargon goes places (e.g. CPU exceptions are nothing to do with "exception handling" for example.) I'd argue that in the modern day programming language exception handling is the type where you have structured control flow dedicated to just the exception handling. Go has stack unwinding with panic and recover, but those are just normal functions, there's no try, no catch, and no throw, and no equivalent to any of those. C also has setjmp/longjmp which can be used in similar ways, but I wouldn't call that exception handling either.
But I think we'll have to agree to disagree on that one, since there's little to be gained from a long debate about what jargon either does or should subjectively mean. Just trying to explain where I'm coming from.
> We could argue what counts as "exceptions"
What is there to debate? An exception, by every definition I have ever encountered, is a data structure that contains runtime information (e.g. a stack trace) to stand in for a compiler error where the compiler was not sufficiently capable of determining the fault at compile time. It couldn't possibly mean anything else in reason.
Of course, we're really talking about "exception handlers", not "exceptions".
> there's no try, no catch, and no throw, and no equivalent to any of those.
There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
Think about it. Go could easily provide syntax sugar that replaces `try { throw() } catch (err) {}` with `try(func() { throw() }).catch(func(err) {})`. That would truly satisfy your requirements in every way. But what, specially, in that simple search and replace operation says "exceptions" (meaning exception handlers)?
> C also has setjmp/longjmp which can be used in similar ways, but I wouldn't call that exception handling either.
Agreed. You could conceivably craft your own exceptions to carry through the use of setjmp/longjmp, but that wouldn't be a language feature. However, Go does have an exception structure as a built-in.
The Wikipedia article about Exception handling[1] does a better job discussing the history and background IMO. Also, obviously when we say "exceptions" in a programming language, we're definitely talking about "exception handling", the word is omitted because it's obvious on context. I'd argue that one's on you if you thought otherwise. (If we're just talking about an "exception", well the Go error object is an "exception", but it's pretty obvious you're not merely talking about that.)
True to my word, I won't argue over the definition itself.
[1]: https://en.wikipedia.org/wiki/Exception_handling_(programmin...
P.S.:
> There can be in name and reasonable equivalency: https://go.dev/play/p/RrO1OrzIPNe I'm not sure what it buys you, though. You haven't functionally changed anything. For this reason, I'm not convinced by the signifaince of syntax.
To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do. For example, Go doesn't have pattern matching; implementing an equivalent feature with closures does not make this any less true.
> The Wikipedia article about Exception handling[1]
What's that old adage? I think it goes something like "The wiki is always accurate—except when it’s about something you know personally." If you don't enough about the topic to discuss it yourself, what are you doing here?
> Also, obviously when we say "exceptions" in a programming language, we're definitely talking about "exception handling"
Not necessarily. Often it is important to discuss the data structure and not the control flow. Strictly, "exception" refers to either the broad concept of exceptional circumstances (i.e. programmer error) or the data structure to represent it. "Exception" being short for "exception handling" where context is clear is fine, but be sure context is clear if you want to go down that road – unless you like confusing others, I suppose.
> well the Go error object is an "exception"
You mean the error interface? That's not an exception. It's just a plain old interface; literally `type error interface { Error() string }`. In fact, the only reason it gained special keyword status is because it being part of the standard library, where it was originally defined in early versions, caused cyclical import headaches. If Go supported circular imports, it would be a standard library definition instead of a keyword today.
The underlying data structure produced when calling panic is an exception, though. It carries the typical payload you'd expect in an exception, like the stack trace.
Of course, errors and exceptions are conceptually very different. Errors are for things that happen in the environment – invalid input, network down, hard drive crash, etc. Exceptions are for programmer mistakes – faults that could have theoretically been detected at compile time if you had a sufficiently advanced compiler. Obviously you can overload exceptions to carry unexceptional information (as you can overload errors to carry exceptional information), and a pragmatist will from time to time, but that's not the intent for such a feature[1].
> To me this is no different than implementing "exception handling" with setjmp/longjmp, just less work to do.
Aside from the fact that there is actually an exception involved. Again, while you might be able to hand roll your own exception data structure in C, it does not provide it for you like Go does. If setjmp/longjmp were paired with an exception data structure of the box, it would reasonably considered exceptions, naturally.
However, the second bit was the real meat of that. A tiny bit of search and replace and you have a system that is effectively indistinguishable from exception handling in languages like Java, Javascript, etc. You haven't explained what about that search and replace, that does not introduce any other new language features or introduce any new concepts, turns what is not exceptions into exceptions.
[1] Java and its offspring's failed experiments in seeing if errors and exceptions could reasonably be considered the same thing excepted.
> the obvious solution is try-catch, Java style.
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...
Currently if you want to return from a function/method you need to type `return` in the source code. And return is a single expr, it can't be chained or embedded, and in almost all cases it exists on its own line in the file. This is an important invariant for Go, even if you don't understand or buy its motivation. `?` would fundamentally change that core property of control flow. In short, chaining is not considered to be a virtue, and is not something that's desired.
That only covers one tiny case among several possible error flows. Why add special syntax for that?
I think the two big things for Go are:
1. Minimalism.
Go has always had an ethos of extreme minimalism and have deliberately cultivated an ecosystem and userbase that also places a premium on that. Whereas, say, the Perl ecosystem would be delighted to have the language add one or seven knew ways of solving the same problem, the Go userbase doesn't want that. They want one way to do things and highly value consistency, idiomatic code, and not having to make unnecessary implementation choices when programming.
In every programming language, there is a cost to adding features, but that cost is relatively higher in Go.
2. Concurrency.
Concurrency, channels, and goroutines are central to the design of the language. While I'm sure you can combine exception handling with CSP-based concurrency, I wouldn't guarantee that the resulting language is easy to use correctly. What happens when an uncaught exception unwinds the entire stack of a goroutine? How does that affect other goroutines that it spawned or that spawned it? What does it do to goroutines that are waiting on channels that expect to hear from it?
There may be a good design there, but it may also be that it's just really really hard to reason about programs that heavily use CSP-style concurrency and exceptions for error handling.
The Go designers cared more about concurrency than error handling, so they chose a simpler error handling model that doesn't interfere with goroutines as much. (I understand that panics complicate this story. I'm not a Go expert. This is just what I've inferred from the outside.)
(2) hasn’t been a problem for Swift or Rust, both of which have the ability to spawn tasks willy nilly. I don’t think we’re talking about adding exceptions to Go, we’re talking about nicer error handling syntax.
(1) yes Go’s minimal language surface area means the thing you spend the most time doing in any program (handling error scenarios and testing correctness) is the most verbose unenjoyable braindead aspect. I’m glad there is a cultivated home for people that tolerate this. And I’m glad it’s not where I live…
The thing is, it’s not actually a major problem. It’s the thing that gets the most complaints for sure, and rubs folks from other languages the wrong way often. But it’s an intentional design that is aware of its tradeoffs. As a 10 year Go veteran, I strongly prefer Go’s approach to most other languages. Implicit control flow is a nightmare that is best avoided, imo.
It’s okay for Go to be different than other languages. For folks who can’t stand it, there are lots of other options. As it is, Go is massively successful and most active Go programmers don’t mind the error handling situation. The complaints are mostly from folks who didn’t choose it themselves or don’t even actually use it.
The fact that this is the biggest complaint about Go proves to me the language is pretty darn incredible.
> As it is, Go is massively successful and most active Go programmers don’t mind the error handling situation. The complaints are mostly from folks who didn’t choose it themselves or don’t even actually use it.
This is a case of massive selection bias. How do you know that Go’s error problem isn’t so great that it drives away all of these programmers? It certainly made me not ever want to reach for Go again after using it for one project.
The language is designed for Google, which hires thousands of newly graduated devs every year. They also have millions of lines of code. In this environment they value easy of onboarding devs and maintaining the codebase over almost everything else. So they are saddled with bad decisions made a long time ago because they are extremely reluctant to introduce any new features and especially breaking changes.
This is a common theme with criticisms of Go.
Relative amateurs assuming that the people who work on Go know less about programming languages than themselves, when in almost all cases they know infinitely more.
The amateur naively assumes that whichever language packs in the most features is the best, especially if it includes their personal favorites.
The way an amateur getting into knife making might look at a Japanese chef's knife and find it lacking. And think they could make an even better one with a 3D printed handle that includes finger grooves, a hidden compartment, a lighter, and a Bluetooth speaker.
FWIW, I have designed several programming languages and I have contributed (small bits) to the design of two of the most popular programming languages around.
I understand many of Go's design choices, I find them intellectually pleasing, but I tend to dislike them in practice.
That being said, my complaints about Go's error-handling are not the `if err != nil`. It's verbose but readable. My complaints are:
1. Returning bogus values alongside errors.
2. Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled.
> Returning bogus values alongside errors.
Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error.
> Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled
I don't see how any good-faith analysis of Go errors as specified/intended by the language and its docs, nor Go error handling as it generally exists in practice, could lead someone to this conclusion.
> I don't see how any good-faith analysis of Go errors as specified/intended by the language and its docs, nor Go error handling as it generally exists in practice, could lead someone to this conclusion.
Let me detail my claim.
Broadly speaking, in programming, there are three kinds of errors:
1. errors that you can do nothing about except crash;
2. errors that you can do nothing about except log;
3. errors that you can do something about (e.g. retry later, stop a different subsystem depending on the error, try something else, inform the user that they have entered a bad url, convert this into a detailed HTTP error, etc.)
Case 1 is served by `panic`. Case 2 is served by `errors.New` and `fmt.Errorf`. Case 3 is served by implementing `error` (a special interface) and `Unwrap` (not an interface at all), then using `errors.As`.
Case 3 is a bit verbose/clumsy (since `Unwrap` is not an interface, you cannot statically assert against it, so you need to write the interface yourself), but you can work with it. However, if you recall, Go did not ship with `Unwrap` or `errors.As`. For the first 8 years of the language, there was simply no way to do this. So the entire ecosystem (including the stdlib) learnt not to do it.
As a consequence, take a random library (including big parts of the stdlib) and you'll find exactly that. Functions that return with `errors.New`, `fmt.Errorf` or just pass `err`, without adding any ability to handle the error. Or sometimes functions that return a custom error (good) but don't document it (bad) or keep it private (bad).
Just as bad, from a (admittedly limited) sample of Go developers I've spoken to, many seem to consider that defining custom errors is black magic. Which I find quite sad, because it's a core part of designing an API.
In comparison, I find that `if err != nil` is not a problem. Repeated patterns in code are a minor annoyance for experienced developers and often a welcome landscape feature for juniors.
Again, you don't need to define a new error type in order to allow callers to do something about it. Almost all of the time, you just need to define an exported ErrFoo variable, and return it, either directly or annotated via e.g. `fmt.Errorf("annotation: %w", ErrFoo)`. Callers can detect ErrFoo via errors.Is and behave accordingly.
`err != nil` is very common, `errors.Is(err, ErrFoo)` is relatively uncommon, and `errors.As(err, &fooError)` is extraordinarily rare.
You're speaking from a position of ignorance of the language and its conventions.
Indeed, you can absolutely handle some cases with combinations of `errors.Is` and `fmt.Errorf` instead of implementing your own error.
The main problem is that, if you recall, `errors.Is` also appeared 8 years after Go 1.0, with the consequences I've mentioned above. Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim that "errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled".
On a more personal touch, as a language designer, I'm not a big fan of taking an entirely different path depending on the kind of information I want to attach to an error. Again, I can live with it. I even understand why it's designed like this. But it irks the minimalist in me :)
> You're speaking from a position of ignorance of the language and its conventions.
This is entirely possible.
I've only released a few applications and libraries in Go, after all. None of my reviewers (or linters) have seen anything wrong with how I handled errors, so I guess so do they? Which suggests that everybody writing Go in my org is in the same position of ignorance. Which... I guess brings me back to the previous points about error-fu being considered black magic by many Go developers?
One of the general difficulties with Go is that it's actually a much more subtle language than it appears (or is marketed as). That's not a problem per se. In fact, that's one of the reasons for which I consider that the design of Go is generally intellectually pleasing. But I find a strong disconnect between two forms of minimalism: the designer's zen minimalism of Go and the bruteforce minimalism of pretty much all the Go code I've seen around, including much of the stdlib, official tutorials and of course unofficial tutorials.
> Indeed, you can absolutely handle some cases with combinations of `errors.Is` and `fmt.Errorf` instead of implementing your own error.
Not "some cases" but "almost all cases". It's a categorical difference.
> Most of the Go code I've seen (including big parts of the standard library) doesn't document how one could handle a specific error. Which feeds back to my original claim that "errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled".
First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types.
But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
> Not "some cases" but "almost all cases". It's a categorical difference.
Good point. But my point remains.
> First, most stdlib APIs that can fail in ways that are meaningfully interpret-able by callers, do document those failure modes. It's just that relatively few APIs meet these criteria. Of those that do, most are able to signal everything they need to signal using sentinel errors (ErrFoo values), and only a very small minority define and return bespoke error types. > > But more importantly, if json.Marshal fails, that might be catastrophic for one caller, but totally not worth worrying about for another caller. Whether an error is fatal, or needs to be introspected and programmed against, or can just be logged and thereafter ignored -- this isn't something that the code yielding the error can know, it's a decision made by the caller.
I may misunderstand what you write, but I have the feeling that you are contradicting yourself between these two paragraphs.
I absolutely agree that the code yielding the error cannot know (again, with the exception of panic, but I believe that we agree that this is not part of the scope of our conversation). Which in turn means that every function should document what kind of errors it may return, so that the decision is always delegated to client code. Not just the "relatively few APIs" that you mention in the previous paragraph.
Even `text.Marshal`, which is probably some of the most documented/specified piece of code in the stdlib, doesn't fully specify which errors it may return.
And, again, that's just the stdlib. Take a look at the ecosystem.
> I absolutely agree that the code yielding the error cannot know (again, with the exception of panic, but I believe that we agree that this is not part of the scope of our conversation). Which in turn means that every function should document what kind of errors it may return, so that the decision is always delegated to client code.
As long as the function returns an error at all, then "the decision [as to how to handle a failure] is always delegated to client [caller] code" -- by definition. The caller can always check if err != nil as a baseline boolean evaluation of whether or not the call failed, and act on that boolean condition. If err == nil, we're good; if err != nil, we failed.
What we're discussing here is how much more granularity beyond that baseline boolean condition should be expected from, and guaranteed by, APIs and their documentation. That's a subjective decision, and it's up to the API code/implementation to determine and offer as part of its API contract.
Concretely, callers definitely don't need "every function [to] document what kind of errors it may return" -- that level of detail is only necessary when it's, well, necessary.
> Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error
But you have to return something to satisfy the function signature's type, which often feels bad.
>> Designing the error mechanism based on the assumptions that errors are primarily meant to be logged and that you have to get out of your way to develop errors that can actually be handled
> I don't see how any good-faith analysis of Go errors as specified/intended by the language and its docs, nor Go error handling as it generally exists in practice, could lead someone to this conclusion.
I agree to a point, but if you look at any random Go codebase, they tend to use errors.New and fmt.Errorf which do not lend themselves to branching on error conditions. Go really wants you to define a type that you can cast or switch on, which is far better.
> Go really wants you to define a type that you can cast or switch on, which is far better.
Go very very much does not want application code to be type-asserting the values they receive. `switch x.(type)` is an escape hatch, not a normal pattern! And for errors especially so!
> they tend to use errors.New and fmt.Errorf which do not lend themselves to branching on error conditions
You almost never need to branch on error conditions in the sense you mean here. 90% of the time, err != nil is enough. 9% of the time, errors.Is is all you need, which is totally satisfied by fmt.Errorf.
> 90% of the time, err != nil is enough
Only if your only desire is to bubble the error up and quite literally not handle it at all.
If you want to actually handle an error, knowing what actually went wrong is critical.
Returning an error -- or, more accurately, identifying an error and returning an annotation or transformation of that error appropriate for your caller -- is a way of handling it. The cases where, when your code encounters an error, that it can do anything other than this are uncommon.
This goes completely against the golang error-handling mindset.
Error handling is so important, we must dedicate two-thirds of the lines of every golang program to it. It is so important that it must be made a verbose, manual process.
But there's also nothing that can be done about most errors, so we do all this extra work only to bubble errors up to the top of the program. And we do all this work as a human exception-handle to build up a carefully curated manual stack trace that loses all the actually-useful elements of a stack trace like filenames and line numbers.
This is just nonsense.
Handling errors this way is possible in only very brittle and simplistic software.
I mean, you're contradicting your very own argument. If this was the primary/idiomatic way of handling errors... then Go should just go the way of most languages with Try/Catch blocks. If there's no valuable information or control flow to managing errors... then what's the point of forcing that paradigm to be so verbose and explicit in control flow?
None.
> Go very very much does not want application code to be type-asserting the values they receive. `switch x.(type)` is an escape hatch, not a normal pattern! And for errors especially so!
A type assert/switch is exactly how you implement Error.Is [^0] if you define custom error types. Sure it's preferable to use the interface method in case the error is wrapped, but the point stands. If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
> You almost never need to branch on error conditions in the sense you mean here. 90% of the time, err != nil is enough. 9% of the time, errors.Is is all you need, which is totally satisfied by fmt.Errorf.
I'd argue it's higher than 9% if you're dealing with IO, which most applications will. Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
[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.
> If you define errors with Errors.New you use string comparison.
With `Errors.New`, you're expected to provide a human-readable message. By definition, this message may change. Relying on this string comparison is a recipe for later breakages. But even if it worked, this would require documenting the exact error string returned by the function. Have you _ever_ seen a function containing such information in the documentation?
As for `switch x.(type)`, it doesn't support any kind of unwrapping, which means that it's going to fail if someone in the stack just decides to add a `fmt.Errorf` along the way. So you need all the functions in the stack to promise that they're never going to add an annotation detailing what the code was doing when the error was raised. Which is a shame, because `fmt.Errorf` is often a good practice.
I was actually referring to the implementation of errors.Is, which uses string comparison internally if you use the error type returned by errors.New and a type cast or switch if you use a custom type (or the cases where the stdlib defines a custom error type).
errors.Is definitely doesn't use string comparison internally:
https://cs.opensource.google/go/go/+/refs/tags/go1.24.3:src/...
Of course it does. What do you think an errors.New contains and how do you figure it compares by value when checking placeholder errors?
The answer is that errors.New just wraps the error message in an errorString struct, and the second line of `is` is a string comparison.
> A type assert/switch is exactly how you implement Error.Is [^0]
errors.Is is already implemented in the stdlib, why are you implementing it again?
I know that you can implement it on your custom error type, like your link shows, to customize the behavior of errors.Is. But this is rarely necessary and generally uncommon..
> If you define errors with Errors.New you use string comparison, which is only convenient if you export a top level var of the error instead of using Errors.New directly.
What? If you want your callers to be able to identify ErrFoo then you're always going to define it as a package-level variable, and when you have a function that needs to return ErrFoo then it will `return ErrFoo` or `return fmt.Errorf("annotation: %w", ErrFoo)` -- and in neither case will callers use string comparison to detect ErrFoo, they'll use errors.Is, if they need to do so in the first place, which is rarely the case.
This is bog-standard conventional and idiomatic stuff, the responsibility of you as the author of a package/module to support, if your consumers are expected to behave differently based on specific errors that your package/module may return.
> Complex interfaces like HTTP and filesystems will want to retry on certain conditions such as timeouts, for example. Sure most error checks by volume might be satisfied with a simple nil check, it's not fair to say branching on specific errors is not common.
Sure, sometimes, rarely, callers need to make decisions based on something more granular than just err != nil. In those minority of cases, they usually just need to call errors.Is to check for error identity, and in the minority of those minority of cases that they need to get even more specific details out of the error to determine what they need to do next, then they use errors.As. And, for that super-minority of situations, then sure, you'd need to define a FooError type, with whatever properties callers would need to get at, and it's likely that type would need to implement an Unwrap() method to yield some underlying wrapped error. But at no point are you, or your callers, doing type-switching on errors, or manual unwrapping, or anything like that. errors.As works with any type that implements `Error() string`, and optionally `Unwrap() error` if it wants to get freaky.
> Unless documented otherwise, a non-nil error renders all other return values invalid, so there's no real sense of a "bogus value" alongside a non-nil error.
Ah yes the classic golang philosophy of “just avoid bugs by not making mistakes”.
Nothing stops you from literally just forgetting to handle ann error without running a bunch of third party linting tools. If you drop an error on the floor and only assign the return value, go does not care.
I know..! Ignoring an error at a call site is a bug by the caller, that Go requires teams to de-risk via code review, rather than via the compiler. This is well understood and nobody disputes it. And yet all available evidence indicates it's just not that big of a deal and nowhere near the sort of design catastrophe that critics believe it to be. If you don't care or don't believe the data that's fine, everyone knows your position and knows how dumb you think the language is.
(not the GP)
Indeed, while not being a fan of how this aspect of Go, I have to admit that it seldom causes issues.
It is, however, part of the reasons for which you cannot attach invariants to types in Go, which is how my brain works, and probably the main reasons for which I do not enjoy working with Go.
Yeah, I mean, Go doesn't see types as particularly special, rather just as one of many tools that software engineers can leverage to ship code that's maintainable and stands the test of time. If your mindset is type-oriented then Go is definitely not the language for you!
> all available evidence
Where is this evidence? Where is the data that I am supposed to believe?
To be fair there are lots of people who have used multiple programming languages at expert levels that complain about go - in the same ways - as well! They might not be expert programming language designers, but they have breadth of experience, and even some of them have written their own programming languages too.
Assuming that all complainants are just idiots is purely misinformed and quite frankly a bit of gaslighting.
"To be fair there are lots of pilots who have flown multiple aircraft at an expert level that complain about the Airbus A380 - in the same ways - as well! They might not be expert airplane designers, but they have a breadth of experience, and even some of them have created their own model airplanes too."
Yes, non-experts can have valid criticisms but more often than not they're too ignorant to even understand what trade-offs are involved.
see there you go again assuming. im talking about people who have written programming languages that are used in prod with millions of users, not people with toy languages.
is the entire go community this toxically ignorant?
You said nothing that indicates you were referring to other expert language designers.
This entire thread is full if amateurs making ignorant comments. So what expert criticisms are you referring to?
You accused me of "gaslighting" and being "toxically ignorant" while I have been entirely civil.
The problem is that error handling is far more complex than you think at first.
The idea that "the happy path is the most common" is a total lie.
a + b
CAN fail. But HOW that is the question!So, errors are everywhere. And you must commit to a way to handle it and no is not possible, like no, not possible to satisfy all the competing ideas about it.
So there is not viable to ask the community about it, because:
a + b
CAN fail. But HOW change by different reasons. And there is not possible to have a single solution for it, precisely because the different reasons.So, you pick a side and that is.
that is weird that they call it a Wiki when it is not a wiki any more - you have to submit changes for approval
unfortunately too much spam but the review process is much lighter
You draw up a list of checkboxes, you debate each individual point until you can check them off, you don't uncheck them unless you have found a showstopping semantics error or soundness hole. Once it is complete it is implemented and then everyone who had opinions about whether it should be spelt `.await` or `/await` or `.await!()` vanishes back into the woodwork from whence they came. Where's the disconnect?
Rust works like this. Sometimes an issue can be delayed for over a decade, but eventually all the boxes are checked off and it gets stabilized in latest nightly. If Go cannot solve the single problem everyone immediately has with the language, despite multiple complete perfect proposals on how to do it, simply because they cannot pick between the proposals and are waiting for people to stop bikeshedding, then their process is a farce.
"despite multiple complete perfect proposals on how to do it"
There is no such a thing.
and this is how rust gains its reputation as an ugly to read language with inconsistent syntax: design by committee
Golang is also ugly, for example some fields start with a capital letter and some do not.
Also I don't understand how to implement transparent proxies in Go for reactive UI programming.
If you don't care about field access just always write fields with uppercase. Any APIs you're using only expose uppercased variables as well, so it'll stay consistent.
The public/private stuff is mostly useful for publishing modules with sound APIs.
you don't Go is explicit about things.
maybe caps for export is ugly, it's not much different from how python hides with _
I find both the golang uppercase lowercase naming scheme and python underscores for private (and __ for *extra* private) to be terrible design choices.
They are hidden functionality, a set of rules which must be remembered. “Make sure to do <weird trick> because that mean <X> in <PL>”
Leave identifier names alone. Packing extra info inside is unnecessary mental burden
> and __ for extra private
That was never its purpose and using it that way is in fact misuse.
Name mangling was added to avoid unintentional conflicts in inheritance scenarios. That’s why it’s static, simple, and well documented.
Also, I recently learned that only leading __ are considered special, whereas both sets of __ go back to being a public method due to the dunder magic methods such as __add__, __eq__, etc
Oh yeah, I didn’t even notice. Much worse than I thought!
Imagine explaining these rules to a beginner learning programming.
It should be mentioned that within the python community using __ for extra private is widely seen as a misfeature that shouldn't've been added and shouldn't be used.
the other option seems worse, i dunno if i can use this identifier unless i have an IDE or i look up the definition every time
"_" does not hide anything in Python. It's just a convention that says "use at your own risk".
Whereas Go has taken the approach of designing everything "later".
It's good to have languages with different approaches to design and and with different design philosophies.
To be fair, Go, under the watch of Google, deemed it a finished product as far as the language goes. "Later" wasn't expected to happen. It was complete already. Only when the outside community took control of the project did it start considering new features.
Isn't "design by popular vote" an extreme version of "design by committee"? Go won't implement error handling syntax because it can't reach community consensus.
Go may take community opinion into account, but just because something is popular doesn't mean the team will accept it. the language designers have the final say, and they will reject popular but bad ideas.
Ugly is subjective, but which part of the syntax is inconsistent?
I tended to disagree on this discussion in the past, but I increasingly no longer do. For example, let's have a look at the new `implicit lifetime capturing` syntax:
fn f(x: &()) -> impl Sized + use<'_> { x }
It's weird. It's full of sigils. It's not what the Rust team envisioned before a few key members left.The only parts of this that I (as someone who has dabbled in rust, but barely interacted with explicit lifetimes, ever) am confused by is
"&()".
And I assume it is similar to some kind of implicit capture group in cpp ("[&]") and "`_", which is a lifetime of some kind. I don't know what the "use" keyword does, but it's not a sigil, and "->", "impl Sized", and "{"/"}" are all fairly self-explanatory.
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.
It's a reference to a unit type. unit is pretty useless (like void in Java). It's sort of a silly thing you wouldn't do in real production code, just whoever wrote this example picked a type that is short to type, would be understood by a Rust programmer, and doesn't require any external context.
"u8" would have done the job fine and actually been readable
It would need to be &u8, as without a reference, you won't have any captured lifetimes, and therefore it wouldn't serve as an example for the capturing lifetime feature. &u8 is also a mostly pointless type in reality.
Your assumption is incorrect, it‘s just a reference to an empty tuple
I don't know if this qualifies as inconsistent, but:
`impl T for for<'a> fn(&'a u8) {}`
The `for` word here is used in two different meanings, both different from each other and from the third and more usual `for` loop.
Rust just has very weird syntax decisions. All understandable in isolation but when put altogether it does yield a hard to read language.
All three have the same underlying idea: do this for every thing of that. In the first case, it's implement a trait for a type. In the second case, it's "for all choices of the lifetime" and for a for loop, it's do something for each element of a collection.
I understand how that seems logical in isolation but it's just not how syntax is usually read by people. It's done so as part of a reading context instead of as separate syntatical tokens. The underlying idea is not the same for the reader because the context is vastly different.
Sure, and I think that's insightful: what you may consider a mess, I may consider orthogonal!
This feels disingenuous. I have a hard time imagining a case where someone would find this confusing.
Rust tends to prefer reusing keywords (in unambiguously different contexts) rather than introducing new ones, because adding a new keyword is a backwards compatibility break that requires a new language edition and creates migration pain for anyone who had an identifier with the same name as the new keyword.
While that's true, all three of these uses pre-date Rust 1.0, so there was total freedom in this case.
Just some examples I thought of:
* Array types have completely different syntax from other generic types
* &mut T has a space between the qualifier and the type, &T doesn’t
* The syntax for anonymous functions is completely different from a function declaration
.await something that looks like a field access but actually does something else
Designs have trade-offs.
In this case, it was important for await and error handling with the ? operator to be readable together.
The order of operations in `await foo()?` is ambiguous, but `foo()?.await` and `foo().await?` have an obvious and clearly visible order. As a bonus, the syntax supports chaining multiple async operations without parenthesis. `fetch().await.body().await` is much nicer to write than `await (await fetch()).body()`.
Since `await` is a reserved keyword, `.await` can't be a field access. Editors with syntax highlighting can easily color it like a keyword.
The problem looking like a field has proven to be total a non-issue in practice. OTOH the syntax avoided major pitfall of visually unclear operator precedence, inconvenience of mixing of prefix and postfix operators, and ended up being clear and concise. It's been such a success, that users have since asked to have more keywords and macros usable in a postfix form.
It's also worth considering that many editors have default snippets that transform something.await into (await something) for languages with prefix await, so it kind of makes sense to cut out the middle man and just make that the syntax to me.
That transformation makes writing both variants similar, but you still need to read `await (await fetch()).body()`, which IMO is much worse.
This might be a valid point if Go wasn’t an atrociously ugly and verbose language with some of the worst design out there.
This is the kind of criticism made by people who've spent less than a few days working with a language. Just glancing at some code from a distance. There's nothing actually wrong with it besides being foreign from what you are used to. After you gain some familiarity, it doesn't look ugly or beautiful it just looks like syntax.
Programming languages are designed systems, they need to make sense holistically. They're not simply collections of tick-boxed features that are expected to be added once their tick-box requirements are satisfied.
> Programming languages are designed systems, they need to make sense holistically.
Of all the languages in common use, golang is the one that makes the least sense holistically. Return values are tuples, but there's nothing that lets you operate on them. Enums aren't actually limited to the values you define, so there's no way to ensure your switch cases are exhaustive when one is added in the future. Requiring meaningful zero values means that your error cases return valid, meaningful values that can accidentally be used when they return with an error.
This opinion is clearly far more arguable than you might think.
> Go cannot solve the single problem everyone immediately has with the language...
What? Survey says 13% mentioned error handling.
And some people actually do prefer it as is.
13% mentioned that error handling was the biggest challenge with using Go. This was not a multiple choice question, but you had to pick one answer. We don't know how many people would consider it challenging. (This is typically why you have a 1-10 scale per choice.)
This doesn't mean the rest of the 87% enjoy it. Honestly, I'd rather the next survey included a question "are you satisfied with the current error handling approach"
I'm as satisfied with the error handling approach as I am for the email address handling approach, the time of day handling approach, the temperature handling approach, etc.
But that doesn't imply that I am satisfied. I do believe there is a lot of room for improvement. Frankly, I think what we have is quite bad. Framing it as something about errors misses the forest for the trees, though.
How would I respond to your query without misleading the reader?
That survey specifically asked for the "biggest" challenge. One could make a compelling argument for the survey answer "learning how to write Go effectively" being an extremely bad option to put on a survey, because it at-least partially catch-alls every other answer. Its no wonder it got first place.
!RemindMe in 25 years.
Did Rust become a clusterfuck like C++?
Is Go as timeless as it was during release?
> complete perfect
This is entirely subjective and paints the Go community as being paradoxical, simultaneously obstinate and wanting change.
The disappointing reality is that Go's error handling is the least terrible option in satisfying the language design ethos and developers writing Go. I have a penchant for implementing V's style of error handling, though I understand why actually implementing it wouldn't be all sunshine and rainbows.
No, actually, an operator that's essentially a macro for this entirely boilerplate operation would be less terrible, exactly the same decision Rust made for the exact same reason. So would Cox's proposal, so would others. Doing nothing, as a permanent solution, because you can't figure out which of the better things you should do, is not a virtue. You may be confusing it with the virtue of holding out on simpler solutions while a better solution is implemented, or the virtue of not solving things which aren't problems, but this is a problem and they have intentionally blocked the solution indefinitely.
Rust's try! macro was† "essentially a macro for this entirely boilerplate operation" but the Try operator ? is something more interesting because in the process they reified ControlFlow.
Because implementing Try for your own custom types is unstable today if you want to participate you'd most likely provide a ControlFlow yourself. But in doing that you're making plain the distinction between success/ failure and early termination/ continuing.
† Technically still is, Rust's standard library macros are subject to the same policies as the rest of the stdlib and so try! is marked deprecated but won't be removed.
It's just simply not the cause that error handling is an "entirely boilerplate operation", nor that any kind of macro in Go "would be less terrible" than the status quo, nor is it true that decisions that Rust made are even applicable to Go. Believe it or not, the current approach to error handling actually does work and actually is better than most/all proposals thru the lens of Go's original design intent.
This code:
foo, err := someExpr
if err != nil {
return nil, err
}
Is entirely boilerplate, and a language feature could generate it (and in Rust, does). This is not the same statement as 'all error handling is boilerplate', which is obviously false, which is why I didn't say that. Condensing that particular snippet down to `?` would be less terrible than the status quo, where the status quo is every function being filled with twenty copies of it drastically reducing readability. The situation is exactly the same as with old Rust, where: let foo = match expr {
Ok(val) => val,
Err(e) => return e,
};
Was entirely boilerplate. Rust noticed that this was a problem and solved it. Go's status quo is not better than pre-`?` Rust's status quo; it does nothing pre-`?` Rust didn't. Go just doesn't solve it.It is not actually the original design intent of Go to make every function 50% boilerplate garbage by LoC. Go is extremely full of 'helpful' happy-path short functions that leave you reimplementing lots of stuff more verbosely the moment you step off the happy path, inclusive of happy paths that do partially the wrong thing. `?` is exactly in line with `iota`, `foo_windows.go`, `flag.Var`, `http.HandleFunc`, etc. I don't know why people respond to literally every Go mistake with 'it's actually not a mistake, you just don't understand the genius', especially since half the mistakes are reverted later and acknowledged as mistakes.
This code:
foo, err := someExpr
if err != nil {
return nil, err
}
Is entirely boilerplate
But you'd never write that, you'd write if err != nil {
return nil, fmt.Errorf("some expr: %w", err)
}
which is _not_ boilerplate, in any sense that would benefit from being mitigated with new syntax or short-cuts.> Condensing that particular snippet down to `?` would be less terrible than the status quo
This simply isn't any kind of objective or agreed-upon truth. Many people, including myself, believe that the status quo is better than what you're suggesting here.
People who are annoyed with Go at some fundamental level, and who largely don't use the language themselves, delight in characterizing `if` blocks related to errors as "boilerplate" that serves no purpose, and needs to be addressed at a language level.
> `?` is exactly in line with `iota`, `foo_windows.go`, `flag.Var`, `http.HandleFunc`, etc.
I've thought on this at length and I have no clue as to what you think the common property between these things might be. A proposed language sigil that impacts control-flow, an existing keyword that's generally not recommended for use, a build-time filename convention, and two unrelated stdlib type definitions?
You say 'you would never', 'generally not recommended', etc., about things that dominate all code in the wild. Perhaps you do not understand the Go vision. Yes, in both Go and Rust, people should add context to errors; and in both Go and Rust, they don't. Cox's proposal provides something slightly smarter than raw `?`, while Rust was designed smarter from the start and it just takes a library to do that (`snafu` or `anyhow`).
> I've thought on this at length and I have no clue as to what you think the common property between these things might be.
They are examples of the common property I specifically stated in the preceding sentence:
> Go is extremely full of 'helpful' happy-path short functions that leave you reimplementing lots of stuff more verbosely the moment you step off the happy path, inclusive of happy paths that do partially the wrong thing.
(In point of fact you shouldn't use fmt.Errorf if you're serious about errors either; it cannot be usefully inspected at runtime. You want an explicitly declared error type for that.)
> Rust was designed smarter from the start
I guess this makes it pretty clear that there's no useful conversation to be had with you on this topic.
> (In point of fact you shouldn't use fmt.Errorf if you're serious about errors either; it cannot be usefully inspected at runtime. You want an explicitly declared error type for that.)
You don't need a discrete error type to allow callers to inspect returned errors at runtime -- `fmt.Errorf("annotation: %w", err)` allows callers to check for sentinel errors via `errors.Is` -- which is the overwhelmingly most common case.
Exactly; `return fmt.Errorf("annotation: %w", err)` is just a log-friendlier version of `return err`. The original wrapped error is meant for runtime inspection.
If Haskell was mainstream and everyone piled in and complained that objects were immutable and it adds so much noise having to deal with that using lenses or state monads or whatever, do we go with democracy or do we say wait.... maybe Haskell was meant to be like this, there are reasons something is a seperate language.
I once had a Go function that, unusually, was _expecting_ an error to be returned from an inner function, and so had to return an error (and do some other processing) if none was returned by the inner function, and return nil if the inner function did return an error.
In a nutshell, this meant I had to do `if err == nil { // return an error }` instead of `if err != nil { ... }`. It sounds simple when I break it down like this, but I accidentally wrote the latter instead of the former, and was apparently so desensitized to the latter construct that it actually took me ages to debug, because my brain simply did not consider that `if err != nil` was not supposed to be there.
I view this as an argument in favor of syntactic sugar for common expressions. Creating more distinction between `if err != nil` (extremely common) and `if err == nil` (quite uncommon) would have been a tangible benefit to me in this case.
Any time I write "if err == nil" I write // inverted just to make it stick out. It would be nice if it was handled by the language but just wanted to share a way to at least make it a bit more visible.
if err == nil { // inverted
return err
}
I do something similar. I leave a comment but with a short comment why it’s inverted.
It’s usually pretty obvious why: eg
if err == nil {
// we can exit early because we don’t need to keep retrying
But it at least saves me having to double check the logic of the code each time I reread the code for the first time in a while.I know diddly/squat about Go, but from similar patterns in aeons past, would "nil == err" work as a way to make it stand out?
Just tried this and it appears to be valid in the compiler, formatter and golangci-lint
https://en.wikipedia.org/wiki/Yoda_conditions
Works especially well in languages that can make assignments in if statements, e.g:
if foo = 42 { }
Thank you, I was unaware of this label. Quite descriptive.
Something slightly more elegant (in my subjective opinion) you could do is write
if !(err != nil) {
Would be nice if code editors colored it differently so it's easier to see.
return nil
would be clearer, I think. Seems like it's the same but would color differently in my editor.
Of course, `if fruit != "Apple" { ... }` would leave you in the exact same situation. Is there a general solution to improving upon this? Seeing it as an error problem exclusively seems rather misguided. After all, there is nothing special or unique about errors. They're just state like any other.
I think its more of a comment that "err != nil" is used in the vast majority of cases, so you start to treat it as noise and skim it.
That reality may make the fundamental flaws of the if statement more noticeable, but at the end of the day the problem is still that the if statement itself is not great. If we're going to put in effort to improve upon it – and it is fair to say that we should – why only for a type named error?
Because the type named error is used in that flawed way orders of magnitude more than any other type. If there were other types that were consistently used as the last return value in functions that short-cirucuited when calling other functions that retuned specific sentinels in their final value when called, there would be reason to do it for them too.
In fact, this is exactly what Rust's ? -operator already does, and something that's obscured by the oddness of using pseudo-tuples to return errors alongside non-error values rather than requiring exactly one or the other; `Result` in Rust can abstract over any two types (even the same one for success and error, if needed), and using the ?-operator will return the value from the containing function if it's wrapped by `Err` or yield it in the expression if it's wrapped by `Ok`. In Go, the equivalent would be to have the operator work on `(T, E)` where `T` and `E` could be any type, with `E` often but not always being an error. Of course, this runs into the issue of how to deal with more than two return values, but manually wrap the non-error values into a single type in order to use the operator would solve that with overall way less boilerplate than what's required currently due to it being rarely needed.
> Because the type named error is used in that flawed way orders of magnitude more than any other type.
That does not give reason to only solve for a narrow case when you can just as well solve for all cases.
> If there were other types that were consistently used as the last return value in functions that short-cirucuited when calling other functions that retuned specific sentinels in their final value when called, there would be reason to do it for them too.
Which is certainly the situation here. (T, bool) is seen as often as (T, error) – where bool is an error state that indicates presence of absence of something. Now that your solution needs to cover "error" and "bool", why not go all the way and include other types too?
Errors are not limited to "error" types. Every value, no matter the type, is potentially an error state. bool is an obvious case, but even things like strings and integers can be errors, depending on business needs. So even if you truly only want to solve for error cases, you still need to be able to accommodate types of every kind.
The computer has no concept of error. It is entirely a human construct, so when handling errors one has to think about from the human perspective or there is no point, and humans decidedly do not neatly place errors in a tightly sealed error box.
> rather than requiring exactly one or the other
That doesn't really make sense in the context of Go. For better or worse, Go is a zero value language, meaning that values always contain useful state. It is necessarily "choose one or the other or both, depending on what fits your situation". "Result" or other monadic-type solutions make sense in other languages with entirely different design ideas, but to try and graft that onto Go requires designing an entirely new language with a completely different notion about how state should be represented. And at that point, what's the point? Just use Rust – or whatever language already thinks about state the way you need.
> but manually wrap the non-error values into a single type in order to use the operator would solve that
I'm not sure that is the case. Even if we were to redesign Go to eliminate zero values to make (T XOR E) sensible, ((T AND U) XOR E) is often not what you want in cases where three or more return arguments are found. (T, bool, error) is a fairly common pattern too, where both bool and error are error states, similar to what was described above. ((T AND U) XOR E) would not fit that case at all. It is more like ((T XOR U) OR (T XOR E)).
I mean, realistically, if we completely reimagined Go to be a brand new language like you imagine then it is apparent that the code written in it would look very different. Architecture is a product of the ecosystem. It is not a foregone conclusion that third return arguments would show up in the first place. But, for the sake of discussion...
> That does not give reason to only solve for a narrow case when you can just as well solve for all cases.
...
This clearly can't be solved "just as well" because nobody can figure out how to do it. The second half of your comment alludes to this, but a lot of what makes this hard to solve are pretty inherent to the design of the language, and at this point, there's a pretty large body of empirical evidence showing that there's not going to be a solution that elegantly solves the issue for every possible theoretical case. Even if someone did manage to come up with it, they're literally saying that they wouldn't entertain a proposal for it at this point! I don't understand how you can come away from this thinking it's realistic that this would get solved in some general way.
> The computer has no concept of error. It is entirely a human construct, so when handling errors one has to think about from the human perspective or there is no point, and humans decidedly do not neatly place errors in a tightly sealed error box.
That's exactly the argument for solving this for what you're calling a "narrow" case. Providing syntax just for (T, E) that uses the zero value for T when short-circuiting to return E would improve the situation from a human perspective, even if it meant that to utilize it for more than two return values you need to define a struct for one or both of T or E. The only objections to it that you're raising are entirely from the "computer" perspective of needing to solve the problem in a general fashion, which is not something that needs to be done in order to alleviate the issues for humans.
> This clearly can't be solved "just as well" because nobody can figure out how to do it.
Fine, but then that means there is no other solution for Go unless you completely change the entire fundamental underpinnings of the language. But, again, if you're going to completely change the language, what's the point? Just use a different language that already has the semantics you seek. There are literally hundreds of them to choose from already.
> That's exactly the argument for solving this for what you're calling a "narrow" case.
Go has, and has had since day one, Java-style exception handlers. While it understandably has all the same tradeoffs as Java exception handling, if you simply need to push a value up the stack, it is there to use. Even the standard library does it when appropriate (e.g. encoding/json). The narrow error case is already covered well enough - at least as well as most other popular languages that have equally settled on Java-style exception handling.
Let me be clear: It is the general case across all types that is sucky. Errors, while revealing, are not the real problem and are merely a distraction.
This is actually an argument against the syntactic changes. Because now if you have the common `if err == nil { return ... }` pattern, then you have _that_ "littering" your code, instead of the syntax.
The current solution is fine, and it seems to be only junior/new to golang people who hate it.
Everyone I know loves the explicit, clear, easy to read "verbose" error handling.
> then you have _that_ "littering" your code, instead of the syntax.
Yes, exactly. The unusual thing _should_ look unusual.
The unusual case does look unusual. == and != are visually very different.
I suspect the real problem here is that the parent commenter forgot (read: purposefully avoided) to write tests and is blaming the tools to drown his sorrow.
https://news.ycombinator.com/item?id=44172285
> [I] was apparently so desensitized to the latter construct that it actually took me ages to debug, because my brain simply did not consider that `if err != nil` was not supposed to be there.
Clearly not different enough.
Tests are just one tool among many that we use to build and evaluate mental models of behaviour. It's equally possible that the parent commenter noticed unusual behaviour _via_ their tests, and took "ages to debug" precisely _because_ they were misreading the code while trying to understand _why_ the tests were failing. A hypothetical syntax highlighter that flagged up to them "hey, you're doing something unusual here - is that intended?" would have helped them in debugging _alongside_ tests.
> Clearly not different enough.
If you take the word as gospel, but why should we? It is hard to believe. As shocking as it may be, not everything you read on the internet is true.
Either way, the fact of the matter is that discussion about code is silly without code. Since I have no knowledge of the actual code in question, which has suspiciously been kept a secret for some reason, I'll open the bidding with this: https://go.dev/play/p/xEnGTmJ_57g — From the output alone, you don't think you'd be able to gain a pretty good idea of what the problem might be?
Feel free to update the code with something more real-worldy if you think the contrivedness of it masks what you are trying to talk about. We had to start somewhere.
Just as a devil's-advocate argument, an IDE + font could syntax-highlight + ligature `if err != nil` (only under Golang syntax mode) into a single compact heiroglyph and fade it into the background — which would in turn make anything that differs from that exact string (like `if err == nil`) now pop out, due to not being rendered that way.
The same logic could apply to the oppositions they cited to the `try` function though; an editor could easily make it stick out to alleviate it blending in when nested inside blocks. This is exactly why nobody ever accidentally confuses `.await` in Rust for a struct field even though from a plaintext perspective it's syntactically identical. If you're going to utilize the editor to carry the heavy weight, you might as well just pick literally any new syntax that replaces all of the extra typing with something more terse.
Good point. Perhaps it could also be solved in an editor with a collapsed notation like ‘if err … {‘
I like nil == err for this case
Nothingburger here, you had a bug, and you fixed it.
All is well, no need to question your language or the meaning of life.
When you make a mistake irl or trip over when walking, do you reconsider you DNA and submit a patch to God?
Sometimes you just gotta have faith in the language and assume it like an axiom, to avoid wasting energy fighting windmills.
I'm not a deep Go programmer, but I really enjoy how it's highly resistant to change and consistent across it's 15 years so far.
I like Go's explicit error handling. In my mind, a function can always succeed (no error), or either succeed or fail. A function that always succeeds is straightforward. If a function fails, then you need to handle its failure, because the outer layer of code can not proceed with failures.
This is where languages diverge. Many languages use exceptions to throw the error until someone explicitly catches it and you have a stack trace of sorts. This might tell you where the error was thrown but doesn't provide a lot of helpful insight all of the time. In Go, I like how I can have some options that I always must choose from when writing code:
1. Ignore the error and proceed onward (`foo, _ := doSomething()`)
2. Handle the error by ending early, but provide no meaningful information (`return nil, err`)
3. Handle the error by returning early with helpful context (return a general wrapped error)
4. Handle the error by interpreting the error we received and branching differently on it. Perhaps our database couldn't find a row to alter, so our service layer must return a not found error which gets reflected in our API as a 404. Perhaps our idempotent deletion function encountered a not found error, and interprets that as a success.
In Go 2, or another language, I think the only changes I'd like to see are a `Result<Value, Failure>` type as opposed to nillable tuples (a la Rust/Swift), along with better-typed and enumerated error types as opposed to always using `error` directly to help with error type discoverability and enumeration.
This would fit well for Go 2 (or a new language) because adding Result types on top of Go 1's entrenched idiomatic tuple returns adds multiple ways to do the same thing, which creates confusion and division on Go 1 code.
My experience with errors is that error handling policy should be delegated to the caller. Low level parts of the stack shouldn't be handling errors; they generally don't know what to do.
A policy of handling errors usually ends up turning into a policy of wrapping errors and returning them up the stack instead. A lot of busywork.
At this point I make all my functions return error even if they don't need it. You're usually one change away from discovering they actually do.
> If a function fails, then you need to handle its failure
And this is exactly where Go fails, because it allows you to completely ignore the error, which will lead to a crash.
I'm a bit baffled that you correctly identified that this is a requirement to produce robust software and yet, you like Go's error handling approach...
On every project I ship I require golangci-lint to pass to allow merge, which forces you to explicitly handle or ignore errors. It forbids implicitly ignoring errors.
Note that ignoring errors doesn't necessarily lead ti a crash; there are plenty of functions where an error won't ever happen in practice, either because preconditions are checked by the program before the function call or because the function's implementation has changed and the error return is vestigal.
Yet the problem still has happened on big projects:
Pedantically, every single one of those examples are a case of unspecified behaviour, not bugs. There may be no meaningful difference to the end user, but there is a big difference from a developer perspective. Can we find cases of the same where behaviour was specified?
> which will lead to a crash
No it won't. It could lead to a crash or some other nasty bug, but this is absolutely not a fact you can design around, because it's not always true.
I just want borgo[1] syntax to be the Go 2 language. A man can dream...
[1]: https://borgo-lang.github.io/ | https://github.com/borgo-lang/borgo
I have to ask, in comparison to what do you like it? Because every functional language, many modern languages like Rust, and even Java with checked exceptions offers this.
Hell, you can mostly replicate Gos "error handling" in any language with generics and probably end up with nicer code.
If your answer is "JavaScript" or "Python", well, that's the common pattern.
In primarily throwable languages, it's more idiomatic to not include much error handling throughout the stack but rather only at the ends with a throw and a try/catch. Catching errors in the middle is less idiomatic.
Whereas in Go, the error is visible everywhere. As a developer I see its path more easily since it's always there, and so I have a better mind to handle it right there.
Additionally, it's less easy to group errors together. A try/catch with multiple throwable functions catches an error...which function threw it though? If you want to actually handle an error, I'd prefer handling it from a particular function and not guessing which it came from.
Java with type-checked exceptions is nice. I wish Swift did that a bit better.
At this rate, I suspect Go2 is an ideas lab for what's never shipping.
This is the right move for Go. I have grown to really love Go error handling. I of course hated it when I was first introduced to the language - two things that changed that:
- Reading the https://go.dev/blog/errors-are-values blog post (mentioned in the article too!) and really internalizing it. Wrote a moderately popular package around it - https://github.com/stytchauth/sqx
- Becoming OK with sprinkling a little `panic(err)` here and there for truely egregious invalid states. No reason forcing all the parent code to handle nonsense it has no sense in handling, and a well-placed panic or two can remove hundreds of error checks from a codebase. Think - is there a default logger in the ctx?
This is just sad. Neither of your supports have anything to do with how dismal Go's error handling is, and neither would be worsened in any way by making it better. If anything they would be improved.
Even PHP has better error handling with the levels, and the @ (at operator) to suppress errors at a callsite.
Even bash has -e :)
Me too. I'll take the higher Loc for the greater certainty of what is going on.
I thought it was clever in C# years ago when I first used to to grok all the try/catch/finally flows including using and nested versions and what happens if an error happens in the catch and what if it happens in the finally and so on. But now I'd rather just not think about that stuff.
rust-style "sum type" errors are values too
But, of course, makes the incorrect assumption that <T, E> are dependent variables. The idiomatic Go approach is much more representative of reality. Tradeoffs, as always.
You have things the wrong way around.
A developer uses Result because T and E are exclusive. If they’re not, they will use something else. And it will be clear to the caller that they are in a rare oddball case.
The idiomatic Go approach makes no provision for such distinctions at all.
> You have things the wrong way around.
No. You have not considered what is going on around you.
> A developer uses Result because T and E are exclusive.
A programming language may be designed around that premise, but that is not the case in the real world (faults in the real world are never binary), and it is certainly not aligned with the design of Go.
> The idiomatic Go approach makes no provision for such distinctions at all.
Naturally. It logically can't because T and E are both considered to be always valid per the fundamentals of the language. The idioms emerged as an extension of those core principles. As before, other languages may take different view, but Go isn't those languages.
That's what makes this changing error handling business so hard. If Go were a completely different language then it would be easy to adopt something like monads, but for the Go we have things like that just don't logically fit and it is not yet clear what does.
I really don't like how this article claims that the primary issue with Go's error handling is that the syntax is too verbose. I don't really care about that.
How about:
- Errors can be dropped silently or accidentally ignored
- function call results cannot be stored or passed around easily due to not being values
- errors.Is being necessary and the whole thing with 'nested' errors being a strange runtime thing that interacts poorly with the type system
- switching on errors being hard
- usage of sentinel values in the standard library
- poor interactions with generics making packages such as errgroup necessary
Did I miss anything?
90% of working professionally in Go is contriving test cases to achieve statement coverage over each error return branch, something no one would ever do in a language with exceptions.
Test coverage as a target metric is stupid.
Maybe so. However, it is really really handy when you are assessing the completeness of your tests using code coverage, and can clearly see unhandled negative paths. And then you can decide whether some of them deserve dedicated tests or not.
why?
Goodhart's law, and because there are code paths not worth testing.
If its not worth testing delete it.
Which target metrics do you consider to be good despite Goodhart's law?
It this really bothers you, convince your team to use courtney and focus on the more relevant error branches: the ones where (1) a novel error value is produced or (2) handled rather than those that simply bubble it up.
> I really don't like how this article claims that the primary issue with Go's error handling is that the syntax is too verbose.
I don't believe this claim is made anywhere.
We've decided that we are not going to make any further attempts to change the syntax of error handling in the foreseeable future. That frees up attention to consider other issues (with errors or otherwise).
writing a small book on the topic and somehow missing it is the point, not that technically the text is claiming it, the subtext is doing it.
Just remember it took FOREVER for Go to support some form of generics. Go evolution happens at a glacial pace. That's a feature, not a bug....to many.
I agree with item #1, but it can be mitigated somewhat with dev tools like errcheck: https://github.com/kisielk/errcheck?tab=readme-ov-file
Agreed, 100%.
We're both Googlers here and this is so disappointing to be let down again by the Go team.
> Going back to actual error handling code, verbosity fades into the background if errors are actually handled. Good error handling often requires additional information added to an error. For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error. In this (admittedly contrived) example, the relative amount of boilerplate is much smaller:
[...]
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
[...]
It's so funny to me to call "manually supplying stack traces" as "handling an error". By the Go team's definition of handling errors, exceptions* "automatically handle errors for you".* in any language except C++, of course
It's funny to me when people see screenfuls of stack traces and remark how clear and useful it is. Perhaps, but do you really need all that? At what cost to your logs? I'd much rather have a one-liner wrapped error that cuts through all the framework and runtime noise. Yes, I can trace just as effectively (usually better)--the wrapping is very greppable when done well. No, in over a decade of writing Go full time, I have never cared about a runtime function or the other usual verbose garbage in my call stack.
> how clear and useful it is. Perhaps, but do you really need all that?
Do I need clear and useful things? Maybe not. Would I like to have them anyway? Yes.
Years ago, I configured a Java project's logging framework to automatically exclude all the "uninteresting" frames in stack traces. It was beautiful. Every stack trace showed just the path taken through our application. And we could see the stack of "caused-by" exceptions, and common frames (across exceptions) were automatically cut out, too.
Granted, I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features...
And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.
> the wrapping is very greppable when done well
Yeah, that's my other problem with it. _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`). (Or use `%v` if I don't want to wrap. Yet another little nuance I find myself needing to explain to junior devs over and over. And don't get me started about putting that in the `fmt` package.)
So anyway, I've seen monstrosities of errors where there were 6+ "statement: statement: statement: statement: statement: final error" that felt like a dark comedy. I've also seen very high-level errors where I dearly wished for some intermediate context, but instead just had "failed to do a thing: EOF".
That all being said, stack traces are really expensive. So, you end up with some "fun" optimizations: https://stackoverflow.com/questions/58696093/when-does-jvm-s...
> _When done well._ Every time I write an `if err != nil {}` block, I need to decide whether to return the error as is (`return err`) or decorate it with further context (`return fmt.Errorf("stuff broke: %w", err)`)
Easy. Always wrap. Wrap with what you were doing when the error occurred.
> I'm pretty sure logback's complexity is anathema to Go. But my goodness, it had some nice features... And then you just throw the stack trace in IntelliJ's "analyze stacktrace" box and you get clickable links to each line in every relevant file... I can dream.
Yeah, despite the proliferation of IDEs for Go in recent years, Go has traditionally been pretty anti- big-iron IDE.
> Easy. Always wrap. Wrap with what you were doing when the error occurred.
Then we're back to having stack frames for framework and runtime code in our error traces.
In my experience doing this, it does not. The wrapping is all yours to write—frameworks and the standard library don’t use it. Their errors would appear as “leaf” errors decorated by the wrapping you, as a team, have added.
People who write frameworks and even the standard library are still Go programmers. And all major projects have their own internal libraries and frameworks. So any Go programmer has to learn when it's better to wrap an error, and when it's better to bubble it up as-is.
Or perhaps the people who wrote the Go standard library don't follow the ideal Go best practices?
I'm not arguing that stack frames are as good as manually written error traces can be - they're clearly not. I am simply amazed that people include "generate error traces" as a form of "handling" an error.
The argument for explicit error values is often something like "it encourages people to actually handle their errors, rather than ignoring them". And on the face of it, this has some merit: we've all seen code that assumes an HTTP request can't fail, and now a small timeout crashes the entire backup procedure or whatever.
But if "handle the error" simply means "decorate it with a trace and return it", then exceptions already do this, then you're really admitting that there is no fundamental difference from a exception, because this is exactly what exceptions do, all on their own. Sure, they produce less useful traces, but that's usually a tiny difference. After all, the argument wasn't "you'll get better stack traces than exceptions give you", it was "people will be more careful to handle errors".
This is also relevant, because if the goal is to get better error traces, that can also be done with exceptions, with just some small improvements to syntax and semantics (e.g. add syntax for decorating a call site with user supplied context that will get included in any exception bubbled from it; add support in an exception to only print non-library stack frames, add support in the language to declare certain variables as "important" and have them auto-included in stack traces - many ideas).
> there is no fundamental difference from a exception
Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with. Exceptions in many languages are gotos, except you don’t know where you are going to and when you might goto. Can this method fail? Who knows! What exceptions can be thrown by this? Impossible to say… better to simply `catch Exception` and be done with it.
> Flow of control is obvious and traceable with explicit errors—they are not some “other” to be dealt with.
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.
Whenever I get an error in NodeJS without a stack trace I am pretty pissed off about it. When my program is failing for some reason I really want to know where it came from, and the stack is hugely helpful in narrowing down the possibility space.
From the Elixir's developer perspective, this is insane. The issue is solved in Erlang / Elixir by functions commonly returning {:ok, result} or {:error, description_or_struct} tuples. This, together with Elixir's `with` statement allows to group error handling at the bottom, which makes for much nicer readability.
Go could just add an equivalent of `with` clause, which would basically continue with functions as long as error is nil and have an error handling clause at the bottom.
From all available evidence, there is no chance in hell Go could adopt a `with` statement.
Go is fascinating in how long it holds out on some of the most basic, obviously valuable constructs (generics, error handling, package management) because The Community cannot agree.
- Generics took 13 years from the open source release.
- 16 years in there isn’t error handling.
- Package management took about 9 years.
There’s value to deliberation and there’s value to shipping. My guess is that the people writing 900 GH comments would still write Go and be better off by the language having something vs. kicking the can down the road.
We already have languages that ship features. Go is a lone lighthouse of stability in a sea of fancy languages. I'll play with your fancy languages, but I build my own projects that I actually use in Go because I can trust that it will keep working for a long time and if/when I need to go back to it to fix something in a couple of years I don't need to re-learn a bunch of crap that might have seeped through dependencies or the stdlib.
> 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.
Go's multiple return is in itself insane from my perspective. You cannot 'do' anything with a function that has multiple return types except assign them to a variable.
The saddest part is that Go's designers decided to use MRV but pretty much just as bad tuples: as far as I can tell the only thing Go uses MRV for which tuple wouldn't be better as is post-updating named return values, and you could probably just update those via the container.
Common Lisp actually does cool things (if a bit niche) things with MRVs, they're side-channels through which you can obtain additional information if you need it e.g. every common lisp rounding functions returns rounded value... and the remainder as an extra value.
So if you call
(let ((v (round 5 2)))
(format t "~D" v))
you get 2, but if you (multiple-value-bind (q r) (round 5 2)
(format t "~D ~D" q r))
you get 2 and 1.You can at least in Go do this:
r, err := f()
r := f()
_, err := f()
No, you can not do the second one.
The other two are completely routine and will work just fine with lists in JS or tuples in Python or Rust (barring a few syntactic alterations).
That's technically not true.
You can pass multiple return values of a function as parameters to another function if they fit the signature.
for example:
func process[T any](value T, err error) {
if err != nil {
// handle error
}
// handle value
}
this can be used in cases such as control loops, to centralize error handling for multiple separate functions, instead of writing out the error handling separately for each function. for {
process(fetchFoo(ctx))
process(fetchBar(ctx))
}
Well, if fetchBar requires fetchFoo to complete successfully, you still somehow have to handle it.
That said, there are libraries out there that implement Result as generic type and it's fine working with them, as well.
I don't see what the hubbub is all about.
What else would you want to do with them? Maybe in rare cases you'd want to structure them into an array or something, but the inverse isn't possible either [e.g. func f(a, b, c int) -> f(<destructure array into arguments>)] so it isn't like it is inconsistent.
Perhaps what you are really trying to say is that multiple function arguments is insane full stop. You can pass in an array/tuple to the single input just the same. But pretty much every language has settled on them these days – so it would be utterly bizarre to not support them both in and out. We may not have known any better in the C days, but multiple input arguments with only one output argument is plain crazy in a modern language. You can't even write an identity function.
have a single return value and if you really need MRV, return as a tuple type, which you could destructure.
(this is what zig does)
But then why accept multiple input arguments? Why not limit to a single argument, accepting a tuple where multiple arguments are necessary, for input too?
Where multiple input arguments are present, not having multiple output arguments is just strange.
Exactly. Why not make multiple argument function call syntatic sugar over a ~single argument tuple call?
What would you need syntax sugar for? If you are going to support multiple arguments you may as well do it properly or don't do it at all. A poor bandaid because you fucked up tuples is not a quality that is to strive for.
Haskellers and Rust fans think they own sum types, and people read their comments and blog posts and believe them, and decide they don't want sum types because they don't want to go down the horrifying Hindley-Milner rabbit hole.
But meanwhile it's just perfectly idiomatic Erlang and Elixir, none of that baggage required. (In fact, the sum types are vastly more powerful than in the ML lineage - they're open.)
I havent followed this argument closely so forgive me if I'm missing relevant discussion, but I dont see why the Rust style isnt just adopted. Its the thing I immediately add now that I have generics in Go.
I only see this blurb in a linked article:
> But Rust has no equivalent of handle: the convenience of the ? operator comes with the likely omission of proper handling.
But I fail to see how having convenience equates to ignoring the error. Thats basically half of my problem with Go's approach, that nothing enforces anything about the result and only minimally enforces checking the error. eg this results in 'declared and not used: err'
x, err := strconv.Atoi("123")
fmt.Println("result:", x)
but this runs just fine (and you will have no idea because of the default 0 value for `y`): x, err := strconv.Atoi("123")
if err != nil {
panic(err)
}
y, err := strconv.Atoi("1234")
fmt.Println("result:", x, y)
this also compiles and runs just fine but again you would have no idea something was wrong x, err := strconv.Atoi("123")
if err != nil {
}
fmt.Println("result:", x)
Making the return be `result` _enforces_ that you have to make a decision. Who cares if someone yolos a `!` or conveniently uses `?` but doesnt handle the error case. Are you going to forbid `panic` too?Go can’t have Result because they don’t have sum types, and they can’t add them because of their bizarre insistence that every type has to have a designated zero value.
> they can’t add them because of their bizarre insistence that every type has to have a designated zero value.
Nothing prevents adding union types with a zero value. Sure it sucks, but so do universal zero values in pretty much every other situation so that's not really a change.
Making it so all sum types have to be nillable would make them dramatically worse (the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL). I guess this is in agreement with your point.
I've read elsewhere that one idea for the zero value of sum types is not making it nillable, but rather making the zero value as the 0th variant of the sum type (and if it has associated data, the zero value of that as well).
It's weird, but does align with design decisions that have already been made.
So if there was an `Option[T]` with variants `None` and `Some[T]`, the zero value would be `None` because that's the zero-th variant
In Scala basically all types are nullable (including Option/Try/Either), but it never really comes up because no one uses nulls (except when wrapping java code) because the language makes it easier to just do the right thing almost all of the time.
In fact the lack of sum types seems to be why you need everything to have a zero value in the first place: because sums are products you need a nonsensical value when the sum is the other type.
> the basic motivating example for sum types is Option, the whole point of which is to get rid of NULL
I don't think that's the case in Go: whereas I got the impression the C# team started souring on default() after generics landed (possibly because nullable value types landed alongside and they found out that worked just fine and there was no reason nullable reference types wouldn't) I don't really get that impression from the Go team, even less so from them still mostly being Googlers (proto3 removed both required fields and explicit default values).
> every type has to have a designated zero value
This bonkers design decision is, as far as I can tell, the underlying infectious cause of nearly every real issue with the language.
> But I fail to see how having convenience equates to ignoring the error.
The convenience of writing `?` means nobody will bother wrapping errors anymore. Is what I understand of this extremely dubious argument.
Since you could just design your `?` to encourage wrapping instead.
> Since you could just design your `?` to encourage wrapping instead.
Which is exactly what Rust does -- if the error returned by the function does not match the error type of `?` expression, but the error can be converted using the `From` trait, then the conversion is automatically performed. You can write out the conversion implementation manually, or derive it with a crate like thiserror:
#[derive(Error)]
enum MyError {
#[error("Failed to read file")
IoError(#[from] std::io::Error)
// ...
}
fn foo() -> Result<(), MyError> {
let data = std::fs::read("/some/file")?;
// ...
}
You can also use helper methods on Result (like `map_err`) for inserting explicit conversions between error types: fn foo() -> Result<(), MyError> {
let data = std::fs::read("/some/file").map_err(MyError::IoError)?;
// ...
}
1. That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping.
2. Idiomatic go type erases errors, so you're converting from `error` to `error`, hence type-directed conversions are not even remotely an option.
`map_err` does not need to be type-directed; you can use an arbitrary function or closure. An enum variant can be used as a function mapping from the variant type to the error type, but we can do any arbitrary transformation:
.map_err(|e| format!("Failed to read file: {e}")?;
But the "idiomatic Go" way of doing things sounds a lot closer to anyhow in Rust, which provides convenience utilities for dealing with type-erased errors: use anyhow::{Result, Context};
fn foo() -> Result<()> {
let data = std::fs::read("/some/file").context("Failed to read file")?;
// ...
}
Yes, I know that, but the argument (which, again, I called dubious) is that in both cases it's much easier to do just e.g.
fn foo() -> Result<()> {
let data = std::fs::read("/some/file")?;
// ...
}
whereas the current morass of Go's error handling means adding wrapping is not much more of a hassle.But of course even if you accept that assertion you can just design your version of `?` such that wrapping is easier / not wrapping is harder (as it's still something you want) e.g. make it `?"value"` and `?nil` instead of `?`, or something.
> That is a global static relationship rather than a local one dynamic one, which is the sense in which Go users use wrapping.
In practice, the error type will be defined quite close to where the conversion is applied, so the static nature of it doesn’t feel too big.
You need to implement from for every type of error then? That seems pretty tedious also.
> The convenience of writing `?` means nobody will bother wrapping errors anymore.
A thread from two days ago bemoans this point:
> I dont see why the Rust style isnt just adopted.
Mostly because it is not entirely clear what the Rust-style equivalent in Go might be. What would Rust's "From" look like, for example?
> What would Rust's "From" look like, for example?
Idiomatic Go type-erases error types into `error`, when there is even a known type in the first place.
Thus `From` is not a consideration, because the only `From` you need is
impl<'a, E> From<E> for Box<dyn Error + 'a>
where
E: Error + 'a,
and that means you can just build that in and nothing else (and it's really already built-in by the implicit upcasting of values into interfaces).> Go type-erases error types
That's clearly not true.
type MyError struct{}
func (MyError) Error() string { return "MyError" }
func main() {
var err error = MyError{}
fmt.Printf("%T\n", err) // Output: main.MyError
}
The idea doesn't even make any sense. How could you even begin to handle errors in a meaningful way if you were unable to discern what type they are?Sorry, I wasnt specific in that part. When I say 'rust style' Im really just referring to a union type of `result | error`, with a way to check the state (eg isError and isResult) along with a way to get the state (eg getResult and getError). Optionally '?' and '!' as sugar.
That said, the other responder points out why the sum type approach is not favored (which is news to me, since like I said I havent followed the discussion)
It's an interesting idea. Right now, you can do something like this:
res := someFunc() // func() any
switch v := res.(type) {
case error:
// handle error
case T:
// handle result
default:
panic("unexpected type!")
}
Then, presumably, a T|error sum type would be a specialization of the any type that would allow you to safely eliminate the default arm of the switch statement (or so I would like to think -- but the zero value issue rears its ugly head here too). Personally, I'd also like to see a refinement of type switches, to allow different variable names for each arm, resulting in something like the following hypothetical syntax: switch someFunc().(type) {
case err := error:
// handle error
case res := T:
// handle result
}
However, there's no real syntactic benefit for error handling to be found here. I like it (I want discriminated unions too), but it's really tangential to the problem. I'd honestly prefer it more for other purposes than errors.To be fair Rust doesn't have sum type it has enums, which I feel like you could do in Go, but I haven't read the arguments.
Enum is what they call it (perhaps to appear more familiar to C++ programmers?), but from a computer science standpoint they are classic sum types.
Technically, Rust has sum types (a.k.a. tagged unions) that use an enum to generate the tag. So, while enumeration is involved, sum types is still a better description of what it is.
In Go, values are to be always useful, so `result | error` would be logically incorrect. `(result, result | error)`, perhaps – assuming Go had sum types, but that's a bit strange to pass the result twice.
Just more of the pitfalls of it not being clear how Rust-style applies to an entirely different language with an entirely different view of the world.
What is useful about the value of `x` in the following code?
x, err := strconv.Atoi("this is invalid")
On the contrary, `x` is _lying_ to you about being useful and you have absolutely no idea if the string was "0" or "not zero"> and you have absolutely no idea if the string was "0" or "not zero"
You do – err will tell you. But in practice, how often do you really care?
As Go prescribes "Make the zero value useful" your code will be written in such a way that "0" is what you'll end up using downstream anyway, so most of the time it makes no difference. When it does, err is there to use.
That might not make sense in other languages, but you must remember that they are other languages that see the world differently. Languages are about more than syntax – they encompass a whole way of thinking about programs.
I think it's worth noting that, while the general consensus has converged around (T, error) meaning T XOR error, it does not necessarily mean that. There are some places that violate this assumption, like the io.Reader and io.Writer interfaces. Especially io.Reader, where you can have (n>0, io.EOF), which also isn't even a proper error condition! (This isn't a big problem, though, since you rarely need to directly call Read or Write).
If a function `func foo() (int, error)` returns a non-nil error, then the corresponding `int` is absolutely invalid and should never be evaluated by the caller, unless docs explicitly say otherwise.
And this is why I prefer exceptions.
Errors are common but they are errors: they absolutely represent an exceptional branch of your control flow every time.
It seems reasonable to ask if that int should even be available in the control flow syntactically.
Errors are just values, same as other values, it's in no way "exceptional" for a caller to get an error back from a call to some other code. If a function can fail it needs to return an error, if a function call fails the caller needs to deal with that error, not difficult stuff here. "Happy path" is no more or less important than "sad path" and both should be equally represented in the source code as written.
That's doctrine. Saying it doesn't make it useful.
A program serves a business need: so it's well recognized that there's a distinction between business logic, and then implementation details.
So there's obviously no such thing as "just an error" from that alone: because "a thing failed because we ran out of disk space" is very different to "X is not valid because pre-1984 dated titles are not covered under post-2005 entitlement law".
All elephants have 4 legs, but not all things with 4 legs are elephants, and a tiger inside the elephant enclosure isn't "just" another animal.
> So there's obviously no such thing as "just an error" from that alone
The point is that all values are potentially errors. An age value, for example, can be an error if your business case requires restricting access to someone under the age of 18. There is nothing special about a certain value just because it has a type named "error", though.
Let's face it: At the root of this discussion is the simple fact that "if" statements are just not very good. They're not good for handling errors, but they're also not good for handling anything else either. It is just more obvious in the case of what we call errors because of frequency.
Something better is sorely lacking, but seeking better only for types named "error" misses the forest for the trees.
You're simply wrong. If I call a function and it fails, then at the base level it doesn't matter if it failed because "no more disk space" or because "input values are invalid" -- the thing failed, in both cases. The caller needs to deal with that failure, in all cases. Now exactly how it deals with that failure might depend on properties of the error, sure, but the control flow of the program is the same in any case.
- It has poor visibility, it hides control flow branches in a single statement / expression. That's one of the reasons Go got rid of the ternary operator in favor of an if statement where each branch has to be on its own line.
- It isn't easily breakpointable.
- It favors "bubbling up" as-is over enriching or handling.
Wait...
x, err := strconv.Atoi("123")
if err != nil {
panic(err)
}
y, err := strconv.Atoi("1234")
fmt.Println("result:", x, y)
> this also compiles and runs just fine but again you would have no idea something was wrongOkay, I don't use golang... but I thought ":=" was "single statement declare-and-assign".
Is it not redeclaring "err" in your example on line 5, and therefore the new "err" variable (that would shadow the old err variable) should be considered unused and fail with 'declared and not used: err'
Or does := just do vanilla assignment if the variable already exists?
It's trickier than that, unfortunately. There has to be at least one new variable on the left side of := but any other variables that already exist in the same scope will simply be assigned to. However, if you use := in a nested block, then the variable is redeclared and shadows the outer-scope variable.
Thanks, I hate it.
This, along with stuff like implicit access modifiers based on case, nil interface confusion, named or multiple return values, channel lockups, and `append` oddities make it ring a bit hollow when people complain that no-one could be expected to google once and learn what snippet a `?` operator expands to. Like I don't even necessarily hate all these things, just please don't pretend it's the heart and soul of minimalism or approachable predictability.
Yeah, people complain about `if err != nil`, but the real issues are tricky rules around variable shadowing, lack of nil safety and immutability, cgo, defer semantics, etc
As I understand it, go has some special handling for this scenario because its so prevalent which special cases reassignment. The linked article touches on it
> There are exceptions to this rule in areas with high “foot traffic”: assignments come to mind. Ironically, the ability to redeclare a variable in short variable declarations (:=) was introduced to address a problem that arose because of error handling: without redeclarations, sequences of error checks require a differently named err variable for each check (or additional separate variable declarations)
... I thought Go's whole deal was that you give up the expressiveness and power of overdesigned languages for simple, clean, "only one way to do it" semantics. That "special cases reassignment" where ':=' is sometimes a shadowing declaration and sometimes a reassignment sounds like the opposite of that.
The language is full of gotchas like that, you're expected to use the tooling to guardrail yourself, because having a proper type system or coherent syntax is "too complicated" (while learning dozens of patterns and weird tricks apparently isn't).
go vet and this massive collection of linters bundled into a single binary are very popular: https://golangci-lint.run
linters will warn you of accidental shadowing, among many other things.
> That "special cases reassignment" where ':=' is sometimes a shadowing declaration and sometimes a reassignment sounds like the opposite of that.
FWIW it is never a shadowing declaration. It is at least one non-shadowing declaration plus any number of reassignments.
The fun part is the tendency to keep reassigning to `err` makes the unused variable largely useless, so it’s just there to be a pain in the ass, and your need a separate lint anyway.
Oh no, you are making the classic mistake of assuming Go’s designers did something that would make sense, rather than picking the most insane possible design in a given situation.
> Its the thing I immediately add now that I have generics in Go.
If you’re willing to share, I’m very curious to see a code example of what you mean by this.
This is from a while back but was the first thing I thought of: https://gist.github.com/J-Swift/96cde097cc324de1f8e899ba30a1...
I ripped most of it off of someone else, link in the gist
Moving every success value to the heap seems like a big loss to me but I don't see an alternative. I think going the interface route also ends up wrapping everything in an even fatter pointer. But at least I get to think "ah maybe this isn't going to get boxed and it will be free".
interface Result[T] {
IsOk(): bool
IsErr(): bool
Unwrap(): T
UnwrapError(): error
}
// Ok is a Result that represents a successful operation.
struct Ok[T] {
Value: T
}
func Ok[T](value T) Result[T] {
return Ok[T]{Value: value}
}
func (s Ok[T]) IsOk() bool {
return true
}
func (s Ok[T]) IsErr() bool {
return false
}
func (s Ok[T]) Unwrap() T {
return s.Value
}
func (s Ok[T]) UnwrapError() error {
panic("UnwrapError called on Ok")
}
// Err is a Result that represents a failed operation.
struct Err[T] {
Reason: error
}
func Err[T](reason error) Result[T] {
return Err[T]{Reason: reason}
}
func (e Err[T]) Error() string {
return e.Reason.Error()
}
func (e Err[T]) IsOk() bool {
return false
}
func (e Err[T]) IsErr() bool {
return true
}
func (e Err[T]) Unwrap() T {
panic(fmt.Errorf("Unwrap called on Err: %w", e.Reason))
}
func (e Err[T]) UnwrapError() error {
return e.Reason
}
> For instance, a recurring comment in user surveys is about the lack of stack traces associated with an error. This could be addressed with support functions that produce and return an augmented error.
Languages with stack traces gives this to you for free, in Go, you need to implement it every time. OK, you may be disciplined developer where you always augment the error with the details but not all the team members have the same discipline.
Also the best thing about stack traces is that it gives you the path to the error. If the error is happened in a method that is called from multiple places, with stack traces, you immediately know the call path.
I worked as a sysadmin/SRE style for many years and I had to solve many problems, so I have plenty of experience in troubleshooting and problem solving. When I worked with stack traces, solving easy problems was taking only 1-2 minutes because the problems were obvious, but with Go, even easy problems takes more time because some people just don't augment the errors and use same error messages which makes it a detective work to solve it.
They still haven't solved shadowing.
a, err := foo()
b, err := bar()
if err != nil { // oops, forgot to handle foo()'s err }
This is the illusion of safe error handling.I’m surprised I don’t see this mentioned more. This is spooky action at a distance at its worst. And it’s not even limited to error handling. Any multi value assignment works like this.
It's fairly obvious when writing Go that `err` is being shadowed and needs to be checked after each expression. You should be wrapping them anyways!
I would be astonished if there isn't an automated tool to check for that at the push of a button. I would be mildly surprised if there isn't a compiler flag to check for it.
Not a compiler check, but staticcheck is widely used:
% staticcheck test.go
test.go:7:2: this value of err is never used (SA4006)
Yeah, as one data point, https://staticcheck.dev/docs/checks/#SA4006 has existed since 2017.
There very much is not. There is a compiler error you can’t disable if a variable is completely unused and that is it.
Generators and Goroutines have keywords/syntax in Golang but now they don't want to pile on more to handle errors. They could have had one single bit of syntactic sugar, "do notation", to handle all three and more if they had considered it from the beginning but it seems too late if the language designers are even aware of it. TFA says "If you’re wondering if your particular error handling idea was previously considered, read this document!" but that document references languages with ad-hoc solutions (C++, Rust, Swift) and does not reference languages like Haskell, Scala, or OCaml which have the same generic solution known as do-notation, for-comprehensions, and monadic-let respectively.
For example instead of
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return err
}
y, err := strconv.Atoi(b)
if err != nil {
return err
}
fmt.Println("result:", x + y)
return nil
}
they could have something like this: func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a)
y <- strconv.Atoi(b)
} yield fmt.Println("result:", x + y)
}
which desugars to: func printSum(a, b string) result[error, unit] {
return strconv.Atoi(a).flatMap(func(x string) result[error, unit] {
return strconv.Atoi(b).map(func(y string) unit {
return fmt.Println("result:", x + y)
}
}
}
and unlike ad-hoc solutions this one bit of syntax sugar, where for comprehensions become invocations of map, flatMap, and filter would handle errors, goroutines, channels, generators, lists, loops, and more, because monads are pervasive: https://philipnilsson.github.io/Badness10k/escaping-hell-wit...I don't think in real code you generally want to return "err" directly, but add some kind of context to made debugging easier. Its the same problem Rusts `?` has.
How do you do that with this suggestion?
I absolutely agree with this, and would be better than all of the other proposals.
Did anyone propose this in one of the many error handling proposals?
yes
> they could have something like this:
Could in some purely theoretical way, but this is pretty much exactly the same, minor syntax differences aside, as virtually every other failed proposal that has been made around this. It would be useless in practice.
In the real world it would have to look more like this...
func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a) else (err error) {
details := collectDetails(a, b)
stop firstConvError{err, details}
}
y <- strconv.Atoi(b) else (err error) {
stop secondConvError{err}
}
} yield fmt.Println("result:", x + y)
}
or maybe something like this func printSum(a, b string) result[error, unit] {
return for {
x <- strconv.Atoi(a)
y <- strconv.Atoi(b)
} yield fmt.Println("result:", x + y)
}
handle(printSum, "x", func(vars map[string]any, err error) {
details := collectDetails(vars["a"].(string), vars["b"].(string))
return firstConvError{err, details}
}
handle(printSum, "y", func(vars map[string]any, err error) {
return secondConvError{err}
}
...because in that real world nobody just blindly returns errors like your contrived example shows[1]. There are a myriad of obvious (and many not so obvious) problems with that.Once you start dealing with the realities of the real world, it is not clear how your approach is any better. It is pretty much exactly the same as what we have now. In fact, it is arguably worse as the different syntax doesn't add anything except unnecessary complexity and confusion. Which is largely the same reason why all those aforementioned proposals failed. Like the linked article states, syntax is not the problem. The problem is that nobody to date knows how to solve it conceptually without completely changing the fundamentals of the language or just doing what you did, which is pointless.
And there is another obvious problem with your code: result[error, unit], while perfectly fitting in other languages designed with that idea in mind, is logically incorrect in the context of Go. They are not dependent variables. It doesn't make sense to make them dependent. For it to make sense, you would, as before, have to completely change the fundamentals of the language. And at that point you have a band new language, making any discussion about Go moot. That said, I think the rest of your idea could be reasonably grafted onto the (T, error) idiom just as easily. However, it still fails on other problems, so...
[1] Which, I will add, is not just speculation. The Go team actually collected data about this as part of their due diligence. One of the earlier proposals was on the cusp of acceptance, but in the end it wasn't clear who – outside of contrived HN comments – would ever use it due to the limitations spoken of above, thus it ultimately was rejected on that basis.
I have no problem with Go’s error handling. It’s not elegant, but it works, and that’s very much in keeping with the pragmatic spirit of Go.
I’m actually kind of surprised that it’s the top complaint among Go devs. I always thought it was more something that people who don’t use Go much complain about.
My personal pet issue is lack of strict null checks—and I’m similarly surprised this doesn’t get more discussion. It’s a way bigger problem in practice than error handling. It makes programs crash in production all the time, whereas error handling is basically just a question of syntax sugar. Please just give me a way to mark a field in a struct required so the compiler can eliminate nil dereference panics as a class of error. It’s opt-in like generics, so I don’t see why it would be controversial to anyone?
I have 2 problems.
It's too easy to accidentally write `if err == nil` instead of `if err != nil`. I have even seen LLMs erroneously generate the first instead of the latter. And since it's such a tiny difference and the code is riddled with `if err != nil`, it's hard to catch at review time.
Second, you're not forced by the language to do anything with the error at all. There are cases where `err` is used in a function that not handling the `err` return value from a specific function silently compiles. E.g.
x, err := strconv.Atoi(s1)
if err != nil {
panic(err)
}
y, err := strconv.Atoi(s2)
fmt.Println(x, y)
I think accidentally allowing such bugs, and making them hard to spot, is a serious design flaw in the language.I guess those are fair criticisms in the abstract, but personally I can’t recall a single time either has caused a bug for me in practice. I also can’t ever recall seeing an LLM or autocomplete mess it up (just my personal experience—I’m sure it can happen).
> It’s opt-in like generics, so I don’t see why it would be controversial to anyone?
It "breaks" the language in fundamental ways — much more fundamental than syntactic sugar for error handling — by making zero values and thus zero initialisation invalid.
You even get this as a fun interaction with generics:
func Zero[T any]() T {
var v T
return v
}
I don’t see how it breaks anything if it’s opt-in. By default you get the current behavior with zero value initialization if that’s what you want (and in many cases it is). But if you’d rather force an explicit value to be supplied, what’s the harm?
> I don’t see how it breaks anything if it’s opt-in?
Zero values are a fundamental, non-optional, "feature" of Go.
> But if you’d rather force an explicit value to be supplied, what’s the harm?
What happens if you use the function above with your type?
Or reflect.Zero?
If it would only complain on struct literals that are missing the value (and force a nil check before access if the zero value is nil to prevent panics), that would be enough for me. In that case, your Zero function and reflect.Zero can keep working as-is.
Then I fail to see the point, that is trivial to lint, just enable the exhauststruct checker.
I wasn't aware of it—will check it out, thanks.
Seems strange that Rust's "?" gets a mention syntax-wise, but nothing is said about sum types coming to Go. Go's verbose error handling and lack of sum types are my only gripes with the language. It would be nice to see both addressed using Rust's Result type as a model.
I was actually bit scared, so read through the whole post and am happy with the conclusion.
Go is very readable in my experience. I'd like to keep it that way.
One of Rob Pike's talks convinced me that the tendency to add features from other languages causes languages to all resemble each other. It's not a bad thing, but it's something to note. Consider the alternative: What if we had different languages for different tasks, new languages appearing from time to time, rather than accreting features onto existing general purpose languages every few years?
He also made the point that if you have two ways of coding something, then you have to choose every time. I've noticed that people disagree about which way is best. If there were only one way, then all of the same problems would be solved with the same amount of effort, but without any of the disagreement or personal deliberation.
Maybe Go should have exceptions beyond what panic/recover became, or maybe there should be a "?" operator, or maybe there should be a "check" expression, or some other new syntax.
Or maybe Go code should be filled with "if" statements that just return the error if it's not nil.
I've worked with a fair amount of Go code, but not enough that I am too bothered by which pattern is chosen.
On the other hand, if you spend more than a few weeks full time reading and editing Go code, you could easily learn a lot of syntax without issue. If you spend most of your career writing in a language, then you become familiar with the historical vogues that surrounded the addition of new language features and popular libraries.
There's something to be said for freezing the core language.
Small nitpick: panic/recover and error handling are orthogonal. Exceptions != errors.
Watching go people complaining about how other languages encourage bubbling errors up is always hilarious to me because there is literally nothing you can do with errors in go except bubble them up, log them, or swallow them.
Even the article considers "handling" an error to be synonymous with "Adding more text and bubbling it up"!
> there is literally nothing you can do with errors in go except bubble them up, log them, or swallow them
You can also add additional context to the error before bubbling it up. But yes, that part of the point. Instead of bubbling them up, the programmer should instead reflect on whether it is better than just log and proceed, or completely swallow them. This is what error handling is about.
I assume you mean languages with exceptions.
You can't bubble up an exception, it's done automatically. That's a very important distinction. You can't make the decision to bubble up or not, because you do not have the required information - you don't know whether an exception can be thrown or not at any point. Therefore, you can't say you're making a decision at all.
Explicit error allows you to be able to make the decision.
No. Exceptions means the default is to bubble up. You can always write special handling code to prevent bubbling it up if you want in a language with exceptions.
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.
I mean, yeah, most of the time what you do is add more text and bubble up. But:
1. The very fact that adding more text isn't really any more verbose than not encourages you to add more text, making errors more informative.
2. A non-negligible amount of times you do something else: carry on, or do something specific based on what kind of error it was. For instance, ignore an error if it's in a certain class; or create the file if it didn't exist; and so on.
Forcing the error handling doesn't seem to me that different than forcing you to explicitly cast between (say) int and int64. Part of me is annoyed with that too, but then I have PTSD flashbacks from 20 years of C programming and appreciate it.
> making errors more informative.
It makes them almost as informative as languages with stack traces! Imagine if Go had a syntax like python's
raise Exception("bad thing happened") from err
I'd love that.No one here mentioned Zig approach there, so I'll share: https://ziglang.org/documentation/master/#try
Zig has try syntax that expands to `expr catch |e| return e`, neatly solving a common use case of returning an error to the caller.
The "Error Return Trace" described in the next section down also seems like an interesting alternative to a stack trace, possibly solving the same problem with less noise.
How well does it work in practice?
It works really, really well, in part because it's also capable of tracking where an error is turned into another. And the error return trace ends where the stack trace begins (when an error is turned into a panic) so you don't even have to know what an error trace is, because it just makes sense intuitively.
Zig 'error types' are a sum type, while Go ones are just values.
And even then this is just the same as the '?' operator Rust uses, which is mentioned in the post.
Go’s restraint in adding new language features is a real gift to its users. In contrast, Swift feels like a moving target: even on a Mac Studio I’ll occasionally fail to compile a simple project. The expanding keyword list and nonstop churn make Swift harder to learn—and even harder to keep up with.
Error handling is one of my favorite parts of Go. The haters can rip `if err != nil { return fmt.Errorf("error doing thing: %w", err) }` from my cold dead hands.
How do you feel about
v, err := foo.Open()
// …
defer func() {
if closeErr := v.Close(); closeErr != nil {
err = fmt.Errorf("while closing %w: %v", err, closeErr)
}
}()
// …
When you’re writing something trivial/pure, Go’s error handling is fine, if maybe a tad bit verbose, but it quickly becomes nightmarish when you start to do nontrivial things that are typical for systems programming.FWIW I love Go, it’s my daily driver for most things. I still think it can get messy far too quickly
Just write `defer v.Close()`? In almost all cases, `close(2)` errors can be safely ignored. Rust also does this: https://github.com/rust-lang/rust/blob/792fc2b033aea7ea7b766...
I don’t think you want to do this for files you’ve opened for writing.
In fact it’s quite common to “commit” on close, at least from what I’ve seen.
Error handling is the thing I hate the most about go. And none of the serious proposals I've seen would remove your ability to continue using `if err != nil`
I have yet to see a proposal that retains what I love about the status quo: conscientious error handling.
The language’s status quo forces everyone to think about errors more deeply than in other languages and acknowledges that the error case is as critical and worthy of the programmer’s attention.
Something like:
x := FallibleFunction() ? err -> return fmt.Errorf("something happened %v", err)
Doesn't really change that, but significantly reduces the amount of noise from error handling boilerplate. And (most importantly to me) reduces the amount of vertical space taken up by error handling code, which makes it easier to follow the happy flow.Reduces the vertical space by vastly increasing the horizontal space and inserting line noise into the syntax. I don't even understand what that code would do.
I think the issue is that "easier to follow the happy flow" basically also implies "easier to ignore the unhappy flow", and this is what a lot of people who have come to like Go's approach react against. We like that the error case takes up space—it's as important to deal with correctly as the happy path, so why shouldn't it take up space?
Is it 3x as important? Because currently, for a single line of happy path code you often have 3 lines of error handling code.
And while handling errors is important, it is also often trivial, just optionally wrapping the error and returning it up to the caller. I agree it is good for that to be explicit, but I don't think it needs to take up more space than the actual function call.
In my experience, the important error handling code is either at the lowest layer, where the error initiall occurs, or at the top level, where the error is reported to the user. Mid level code usually just propagates errors upwards.
> forces everyone to think about errors more deeply than in other languages
Not really. Rust also forces you think deeply about errors but don't bother you with verbose syntax. I think Swift was also similar.
You don't use gofmt?
The whole point of gofmt is that you type in whatever you like and gofmt sorts it out for you. So if you're typing your code in a HN comment box, you'd surely just enter it roughly like that - though i recommend not even including the syntactically irrelevant spaces shown here. Your space bar does have a MTBF you know, and it's measured in activations... let gofmt take the strain. It can waste your screen space for you!
gofmt is the good bit about working in Go. Pretty much everybody uses it, and so you can use it too. Some other languages have similar tools, but they're not as pervasive, so it's far too easy to end up in a situation where you can't use the tool because it would just make too much of a mess of the inconsistently manually-formatted stuff that's already there.
rustfmt is at least as pervasive as gofmt.
I used to hate the repetitive nature of Go’s error handling until I was burned by bad/mysterious error messages in production.
Now my error handling is not repetitive anymore. I am in peace with Golang.
However I 100% get the complaint from the people who don’t need detailed error messages.
You probably wouldn't have had the bad/mysterious error messages in production if they were just stack traces to begin with.
Stack traces are full of noise by comparison and don't have context added by the programmer at each frame. For me, Go error chains are much easier to work with. I can see the entire flow of the error at a glance, and can zero in on the relevant call in seconds with a single codebase search.
Stack traces are not so long that you can't find the information you need in them, and actually just like go in some languages you can elect to add context to your stack trace (e.g. in python by raising an error from another error).
My experience in go was opposite of yours. The original devs (who were long gone) provided no information at all at the error site and I felt lucky even to find the place in the code that produced the error. Unfortunately the "force you to handle errors" idea, while well intentioned, doesn't "force you to provide useful error handling information", making it worse than stack traces by default.
You're right that its usefulness is contingent on adding helpful context to errors. Without that, I might agree that stack traces are better.
They admit it's contrived, but this isn't very convincing
func printSum(a, b string) error {
x, err := strconv.Atoi(a)
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
y, err := strconv.Atoi(b)
if err != nil {
return fmt.Errorf("invalid integer: %q", b)
}
fmt.Println("result:", x + y)
return nil
}
It's not adding anything that the Atoi function couldn't have reported. That's a perfect case for blindly passing an error up the stack.It does, it says which one of the two integers was incorrect
Why can't Atoi report that?
I think Atoi actually does say that, but it's just a toy example. Most often functions outside the standard library don't contain the arguments in their error values
IMO: The position of "we aren't sure what the right approach to improvement is so we aren't going to do anything" has killed far bigger and more important projects, companies, and even countries, than Golang. Adapt or die; the world isn't going to wait for you.
(I love the Go team, and appreciate everything they do. I'm just sad to see a language I used to love fail to keep pace with many of the other options out there today.)
> Adapt or die
It's not my impression Go is dying. Seems rather overblown.
And this "but $other_lang has it! You must have it! Adapt or die!" type of reasoning is how you end up with C++.
I never said Go is dying. I said go must adapt, or it will die. That's future-tense.
Sure, you can end up with C++ (which is still by some measures the most popular programming language in the world, so that's not a bad place to be). You can also end up with Rust, or Kotlin, or any one of the literally every other programming languages in any ranking's Top 30, all of which have more ergonomic error handling.
A better example in the opposite direction is Java: Its a language that spent years refusing to adapt to the ever-changing needs of software engineers. Its legacy now. That is not Go's present, but it is Go's future at its current pace. Still powering a ton of projects, but never talked about except in disdain like "ugh that Go service is such tech debt, can we get time modernize it next sprint". I don't want that for the language.
Changing too quickly is a much bigger problem. It may not be ideal, but I think leaning towards being slow makes sense in this context.
Every person/company using Go chose to use it knowing how errors are handled.
Each new way of error handling seems to upset a large number of users, some of which may not have chosen Go had the newer system been in place originally.
If it is impossible to know which choice is correct, at least everyone has some baseline level of acceptance for the status quo.
Changing too quickly is not a problem. Changing too quickly can lead to problems.
I don't agree that the problems it leads to are bigger problems than stagnation. I also don't believe they're smaller problems; sorting the problems by size is intractable, as it is situation dependent.
The challenge is in the definition of "too quickly"; if fifteen years of stagnation in addressing more productive error handling is the "right pace" of innovation, or lack-there-of; is twenty years? Thirty years? One hundred years? How do you decide when the time is right? Is the Go team just waiting out the tides of the Vox Populi, and maybe one day a unified opinion from the masses will coalesce?
That's weak.
Is it really fair to say a language is stagnating if it does not re-invent itself every ten years to match whatever language features are popular at the time?
> How do you decide when the time is right?
When people are migrating away from Go because of the error handling.
> maybe one day a unified opinion from the masses will coalesce?
Maybe. What is the alternative? If there are five alternative error handling proposals each with support from 20% of users, should they pick one at random and upset 80% no matter what?
Go adoption has only increased over time.
There's no methodologically sound way to measure its popularity available to most people to make an argument on its popularity one way or the other. That's why I didn't, and that's why you shouldn't either.
If you say so, but the concern you floated (The position of "we aren't sure what the right approach to improvement is so we aren't going to do anything" has killed far bigger and more important projects, companies, and even countries, than Golang. ) is fundamentally silly, given how long Go has been in business and seemingly thriving while applying this approach to a few select, controversial features.
If it was going to be killed by this approach, it would now be dead.
In this particular case the argument seems to be that there is not even consensus improvement is NEEDED. If you cant even agree on that then how do ever hope to agree to a change in the general sense.
Snip:
if err != nil {
return fmt.Errorf("invalid integer: %q", a)
}
Snip. No syntax for error handling is OK with me. Spelling out "hey I actually do need a stack trace" at every call site isn't.You actually don't. In mistakes #52 "Handling an error twice" from the book "100 Go Mistakes and How to Avoid Them" by Teiva Harsanyi, the recommendation is to either return an error to the caller or handle the situation (and maybe logging it). Sometimes you wnat some extra information as to why it's an error, so you wrap it.
Go forces you to be explicit about error handling. Java syntax is not that much better. JavaScript, Kotlin, Swift,... is more about avoiding null pointer exception than proper error handling.
I return the error to the caller. The caller returns it to their caller. 5 frames up, someone gets a [syscall.EINVAL], and has to figure out what to do about it. Perhaps it's time to log it?
If I had to write my own "100 mistakes" book, "assuming the callee knows what to do" would be somewhere in the top 20, down below "I won't need to debug this".
It's all about designing software. The callee is the one encountering the error, not any of the caller up in the stack trace. Somewhere in the call chain, there's a need to take a decision.
So you, as the developer, decide where that needs to be. It may be at the callee level (like an exponential retry) or at the caller level (display an error message). In the later case, you may want to add more information to the error data block, so that the caller my handle the situation appropriately. So if you want tracing, you just need to wrap the error and returns it. Then your logging code have all the information it needs: like
[error saving file [permission error [can't access file]]]
instead of just [syscall.EINVAL].> So if you want tracing, you just need to wrap the error and returns it.
Here we go, fifth time we're both spelling this one out. This thread is now a meta-self-joke.
> After so many years of trying, with three
> We didn’t have a better solution at that time and didn’t pursue syntax changes for error handling for several years.
3 is small, and it seems like there were more years of not trying at all.
And the example of one proposal is telling. It had a major control flow issue, so got rejected, but then there is a recent proposal fixing those issues, and the post just goes on without further elaboration?
> Unfortunately, as with the other error handling ideas, this new proposal was also quickly overrun with comments and many suggestions for minor tweaks, often based on individual preferences.
How is this unfortunate? If the tweaks are good, update the proposal using them, so that's very fortunate? If they aren't, explain why there rejected? Or do you only expect a binary yes/no on the proposal as originally published?
I mean, no wonder with such an approach the only way forward is to give up.
I have zero complaints about Go's error handling and I'm happy with the decision!
> Of course, there are also valid arguments in favor of change: Lack of better error handling support remains the top complaint in our user surveys.
Looking at that survey, only 13% mentioned error handling. So that means 87% didn't mention it. So in that sense, perhaps not too much weight should be given to that?
I agree the verbosity fades into the background, but also feel something better can be done, somehow. As mentioned there's been a gazillion proposals, and some of them seem quite reasonable. This is something where the original Go design of "we only put in Go what Robert, Ken, and Rob can all agree on" would IMHO be better, because these type of discussions don't really get a whole lot better with hundreds of people from the interwebz involved. That said, I wasn't a fan of the try proposal and I'm happy it didn't make it in the language.
And to be honest in my daily Go programming, it's not that big of a deal. So it's okay.
"biggest challenge", it's not multiple choice
Here's a non-syntactic suggestion. Could we just patch gofmt to permit this:
if err != nil { return nil, err; }
as a well-formatted line of go? Make it a special case somehow.My only big problem with if err != nil is that it eats up 3 lines minimum. If we could squish it down to 1, I'd be much more content.
The rub there is that you'll have varying styles in which error handling statements appear in your code. With simplistic instances appearing one way and less simplistic ones appearing similarly or differently depending on where each lands on the complexity spectrum. The idiomatic approach is for all instances to be written in the same way regardless of technical nuances.
All of that aside, I've come to learn that passing errors up the call stack without any wrapping or handling is a code smell. It is less than useless for me to attempt setting the value of cell A1 in an Excel sheet to "Foo" and then receive an out-of-range error because the developer made no attempt to even inform me of the context around the error. Let alone handling the error and attempting to correct state before giving up.
In my Excel example, the cause of the error was a data validation problem a couple columns over (F or so). The error was legitimately useless in troubleshooting.
How would you set a breakpoint on the error return case?
Inline breakpoint, the same way you set a breakpoint on an expression in any language
You wouldn't. Rust's ? operator doesn't permit that either. If you need to put a breakpoint there, put a line break there.
One reason I consider Rust's approach worse than Go's.
What if I wrap the error? Should that be squashed to one line? What is the heuristic?
Sure, why not? Let the programmer decide if they want one line or three lines; the tool can permit both. Gofmt is line-length-agnostic—breaking up long lines is already considered to be the programmer's responsibility.
go fmt already changes one-liner if statements to be multi-line
you are proposing changing this, making code look different based on differences of opinions between developers
go fmt is actually one of the top rated features of Go, it makes everyone's code look the same, everyone has nitpicks about it, yet by and large it is one of the most loved things about Go. Breaking this is even less likely than changing error handling
The code already looks different based on different developers' opinions. Gofmt already allows this:
fmt.Printf("%s %s %s %s\n", arg1, arg2, arg3, arg4)
and this fmt.Printf("%s %s %s %s\n",
arg1, arg2, arg3, arg4)
and this fmt.Printf(
"%s %s %s %s\n",
arg1,
arg2,
arg3,
arg4,
)
and will not alter any of the above. All I want is similar ambiguity around one-line `if` statements. That's not so crazy.> To be more precise, we should stop trying to solve the syntactic problem
Fixing the problem purely from a syntactic perspective avoids any unexpected semantic changes that could lead to irreconcilable disagreements, as clearly demonstrated by the infamous try proposal. Very simple syntactical changes that maps clearly to the original error handling has the advantage of being trivial to implement while also avoids having the developers needing to learn something new.
I am very entertained by this. The Golang community bikeshedded their way into the status quo. Hahaha, I have to say that's a pretty good move by the steering org. Punishes bikeshedding.
This is a very good way to fight back against design by committee.
It could be seen as a big waste of time, but it sets a good precedent.
Go's error handling fade away after a month of consistent use, I would not risk backwards compatibility over this. In fact, I like the explicit error handling.
The proposals are not backwards incompatible. And just because you get used to something doesn't mean it is good.
And FWIW, my hatred of go error handling has not diminished with increased usage.
> I like the explicit error handling
You mean "verbose error handling". All other proposals are also explicit, just not as verbose.
Watching the process of thinking about this from the outside, somehow reminds me of my experience on the inside of the Python community trying to figure out packaging.
Okay here's my idea, not found on the list in the article, what do you think:
You add a visualization sugar via an IDE plugin that renders if/else statements (either all of them or just error cases) as two separate columns of code --- something like
x = foo();
if (x != nil) | else
<happy case> | <error case>
And then successive error cases can split further, making more columns, which it is up to the IDE to render in a useful way. Underneath the representation-sugar it's still just a bunch of annoyingly nested {} blocks, but now it's not annoying to look at. And since the sugar is supported by the language developers, everyone is using the same version and can therefore rely on other developers seeing and maintaining the readability of the code in the sugared syntax.If the error case inside a block returns then its column just ends, but if it re-converges to the main case then you visualize that in the IDE as well. You can also maybe visualize some alternative control flows: for instance, a function that starts in a happy-path column but at all of its errors jumps over into an error column that continues execution (which in code would look like a bunch of `if (x=nil) { goto err; }` cases.
Reason for doing it this way: logical flow within a single function forms a DAG, and trying to represent it linearly is fundamentally doomed. I'm betting that it will eventually be the case that we stop trying to represent it linearly, and we may as well start talking about how to do it now. Sugar is the obvious approach because it minimizes rethinking the underlying language and allows for you to experiment with different approaches.
I think that IDE functionality is fine for writing code, but shouldn’t be imposed for the UX of reading code, because code reading also happens a lot outside of IDEs, because it constrains the choice of editors, and because it creates fundamentally different “modes” of source code presentation. The visualizations will start to appear in comment threads like this one, and in other publications on the web, but copying them and pasting them into an editor will not work (will be invalid syntax). It creates unnecessary complications across the whole ecosystem. Language syntax should stand on its own, and shouldn’t need crutches like that to make it ergonomic to read.
I have the opposite opinion, I guess! We've been trying to solve everything with language syntax for a long time and it's a bit of a dead end, as the OP shows. Better to start trying new things.
Anyway you can always copy paste it in the normal linear format.
I would simplify that to
x = foo() ||| <error case>
<happy case>
(With the specific symbol used in lieu of ||| to be decided)That is shorter and keeps the happy path unindented, even if it has additional such constructs, for example
x = foo() ||| return Error(err, “foo failed”)
y = bar() ||| return Error(err, “bar failed”)
"Not adding extra syntax is in line with one of Go’s design rules: do not provide multiple ways of doing the same thing"
And so any change to any existing functionality is a breaking change that invalidates all code which uses that functionality? That design rule smells like hubris on the part of Go's designers. It only works if all future changes are extensions and never amendments.
Even if the decision to close the door on this sucks I think they are correct - this is not a syntax problem. Adding sugar will not fix fundamental issues w/ Go's error handling.
They need to add/fix like 5-6 different parts of the language to even begin addressing this in a meaningful way.
> When debugging error handling code, being able to quickly add a println or have a dedicated line or source location for setting a breakpoint in a debugger is helpful.
On the flip side, you can't have exception breakpoints in Go.
Gauntlet, the CoffeeScript of golang, seems to have an interesting approach:
https://gauntletlang.gitbook.io/docs/advanced-features/try-s...
It saddens me that the conclusion is
"it's been like this for this long now"
and "no one could ever agree to something"
leads to this amount of verbosity. Any of these keywords approach, `try`, `check`, or `?` would have been a good addition, if they kept the opportunity to also handle it verbosely.The argument that LLM now auto-completes code faster than ever is an interesting one, but I'm baffled by such an experienced team making this an argument, since code is read so many more times than it is written; they clearly identify the issue of the visual complexity while hand-waving the problem that it's not an issue since LLM are present to write it - it completely disregards the fact that the code is read many more times that it is written.
Visual complexity and visual rhythms are important factors in a programming language design, I feel. Go is excruciatingly annoying to read, compared to Dart, C, or any cleaner language.
Ultimately, it's just one of those "meh, too hard, won't do" solution to the problem. They'll write another one of those posts in 5 years and continue on. Clearly these people are not there to solve user problems. Hiding behind a community for decision making is weak. Maybe they should write some real world applications themselves, which involves having these error checks every 2nd lines.
At this point I wouldn't be upset if someone forked Go, called it Go++ and fixed the silly parts.
It's a shame they haven't polled people who refuse to use the language. No exceptions? Maybe it's time to revisit that decision.
I like Go. I use it at work and I find it pretty pragmatic.
But I can't stand the verbosity of the error handling. It drives me nuts. I also can't stand the level of rationalising that goes on when anyone dares to point out that Go's error handling is (obviously) verbose. The community has a pigheaded attitude towards criticism.
It also grinds my gears, because I really like that Go is in most other ways, simple and low on boilerplate. It's not unusual to see functions that are 50% error handling where the error handling actually DOES NOTHING. That's just insane.
All they have to do is to expand generics to support generic methods so we can have monadic operations like in C++'s std::expected or Rust's Result, like
func (r Result[T, E]) AndThen[OtherT any](func(T) Result[OtherT, E]) Result[OtherT, E] { ... }
which would enable error handling like sum := 0
parseAndAdd := func(s string) (func(string)Result[int, error]) { /* returns func which parses and adds to sum */ }
return parseAndAdd(a)().AndThen(parseAndAdd(b))
There's a reason why every other language is converging to that sort of functional setup, as it opens up possibilities such as try-transform generics for ranges.IMHO, the actual problem with go error handling isn’t the error handling at all — it’s that multiple return values aren’t a first class construct. With proper tuple handling and Go’s approach to generics, a lot of these issues would just disappear.
This also grinds my gears when converting multiple-return functions to returns-a-channel functions. Generics help with that now.
Hm, I would agree with this 2 years ago; but now I see how great both iterators and generics were despite my initial personal objections, so I am thinking we should give some less verbose error handling a chance. But alas
I don't use Go, but I actually like Go's error handling and I think multiple return values is a better solution than any other language I've used. So much so, I've basically adopted it in my c++ code using std::pair. Errors are a value, and the abstraction over that is unnecessary in my opinion. Rust's result type is just syntactic sugar around the multiple return value approach. I don't care for the syntactic sugar, and doing many things in few lines of code isn't valuable to me, but I suspect this is why people love rust's error handling.
> Rust's result type is just syntactic sugar around the multiple return value approach
That's really not true. Multiple return values means you always need to return some return value and some error value, even if they are dummy values (like nil). While a result type / sum type genuinely only contains one branch, not the other.
If you had a language that didn't have nil, it would genuinely be impossible to emulate sum type like behavior on top of multiple return values. It serves as an escape hatch, to create a value of some type when you don't actually have a meaningful value to give.
std::variant / std::expected / std::optional aren't syntactic sugar for std::pair either.
I like that it we are forced to deal with the error. It seems to make the code more predictable in some sense.
But I do loath the littered if err != nil { return err }
maybe a better autocomplete in the IDE/editor is a good compromise.
Ah, shame. `foo := someFallibleMethod()?` would have been nice for when you don't want to handle/wrap the error.
I'm not super strongly against the constant error checking - I actually think it's good for code health to accept it as inherent complexity - but I do think some minor ergonomics would have been nice.
Error handling is some of the least fun parts of writing code. In all languages.
But in any case, why so much fear of being wrong?
> we have fine-grained control over the language version via go.mod files and file-specific directives
And that may be the real truth of it: Error handling in Go just isn't ... that much of a problem to force action?
> 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.
This is also my feeling. And I wouldn't even have minded the ? syntax.
It seems that now that Ian's left the rest of the Go team is just being honest about what they are willing to spend their time on.
And I'm more than fine with that, because look at this comment section. You can't please everybody.
Another way of potentially addressing this problem: add some kind of macro facility to Go – maybe take inspiration from Rust
This could be used to solve both "syntactic support for error handling", and also various other common complaints (such as lack of first class enums), without "polluting" the core language – they'd be optional packages which nobody would have to use if they don't want to
Of course, if one of these optional packages ever became truly prevalent, you could promote it to the standard library... but that would involve far less bikeshedding, because it would be about making the de facto standard de jure... and arguably, letting people vote with their feet is a much more reliable way of establishing consensus than any online discussion could ever be
This is such a great move. Faith restored in the language after the generics debacle.
> generics debacle
Why debacle?
Everything is fine
I dream if err, if err dreams me.
Slowly reinventing exceptions seems to go against the spirit of Go. That is what you read is what you get.
Haskell solves this with the do notation, but the price is understanding monads. Go also aims to be easy to understand.
The proposals aren't reinventing exceptions. They are just making more ergonomic syntax for doing the same thing go error handling does today.
You can argue that is what an exception is. A more ergonomic way to deal with errors that handles propagation for you.
That doesn't mean that any ergonomic way to handle errors is necessarily an exception system. Rust's ? operator is more ergonomic than go's error handling, but it is definitely not an exception system.
IDE's could help and just hide standard `if err != nil return fmt.Errorf("sh: %w", err)` blocks for us, or show them in some distinct way, like question mark after the statement.
I'm so happy that Rust sorted this out way early in its lifetime. Error handling is so nice there.
Rust is basically someone looking at ML languages and thinking what if we did that but just worse in every way.
Can't believe we're going to get GTA 6 before an agreed upon (cleaner) error handling pattern in Go.
I have hard time understanding why they didn't go with
func printSum(a, b string) error {
x := strconv.Atoi(a) or {
return error
}
y := strconv.Atoi(b) or {
return error
}
fmt.Println("result:", x + y)
return nil
}
or something along these lines...> Unfortunately, as with the other error handling ideas, this new proposal was also quickly overrun with comments and many suggestions for minor tweaks, often based on individual preferences. Ian closed the proposal and moved the content into a discussion to facilitate the conversation and to collect further feedback. A slightly modified version was received a bit more positively but broad support remained elusive.
> After so many years of trying, with three full-fledged proposals by the Go team and literally hundreds (!) of community proposals, most of them variations on a theme, all of which failed to attract sufficient (let alone overwhelming) support, the question we now face is: how to proceed? Should we proceed at all?
> We think not.
You need to assign the error to a variable, probably. So it would have to be more something like:
n := strconv.Atoi(s) or |err| {
return fmt.Errorf("foo: %w", err)
}
n := strconv.Atoi(s) or |err| {
if !errors.Is(err, pkg.ErrFoo)
return fmt.Errorf("foo: %w", err)
}
}
Just "error" (which shadows the built-in type) won't really work.I'm just making up syntax here to illustrate the point; doesn't look too brilliant to me. A func might be a bit more "Go-like":
n := strconv.Atoi(s) or func(n int, err error) {
return fmt.Errorf("foo: %w", err)
}
All of this is kind of a moot point at Robert's blog post says that these proposals won't be considered for the foreseeable future, but IMHO any error handling proposal should continue to treat errors as values, which means you should be able to use fmt.Errorf(), errors.Is(), mylogger.Error(), etc.Seems like it's only worth the cost of the change if it removes all three verbose lines per check, this only removes one.
If you do this, returning the error last is now part of the language rather than a convention. You’d be making a pretty large language change.. and for what? One line of code that’s already folded away by any modern editor?
Oh cool, you've reinvented perl.
Go’s philosophy is the problem: sometimes you need a few ways to do the same thing. And that’s okay. Sometimes the flow you’re trying to express in code just needs the error to go up and get handled at the top. Sometimes you want to act on it. Right now all you can do is act on it. So all the times you don’t need to act on it have to suffer? I call profuse bullshit. There is 100% a way to add terse compiler-enforced error handling (for the love of all things holy if nothing at all changed with the syntax at least there should be consensus that it’s a serious problem that the compiler can’t tell you “oops you forgot to handle that error”) to be added in a way that doesn't bifurcate API surfaces. You just have to do it even in the face of dissent. You have to compromise on your values for the good of the language. What is the damn problem with ? literally expanding to `if err != nil { \n return err \n }`? Just take small steps that improve everyone’s life. If you look for excuses not to improve error handling you’ll find them. Sadly that appears to be what has happened here. The entire language suffers because a vocal minority are sadists. Is it time for me to come back to Go? I think not.
The way elixir conventionally uses tuples and pattern matching is really good.
It's a pale shadow of an actual Option/Result type, though, and the way most community and professional Elixir code uses ok-tuples is hardly what I would call rigorous. The with special form leads to sloppy pass-the-buck design IME, and nothing in the language design or Credo ruleset is likely to steer anyone to do better.
awesome I love noisy boilerplate in my code, it isn't annoying at all
Oh No! Is a much better name for error handling.
considering the recent post [1] on system correctness in amazon quoting "In 2014, Yuan et al. found that 92% of catastrophic failures in tested distributed systems were triggered by incorrect handling of nonfatal errors."
perhaps it's a good thing that error handling is so explicit, and treated as a regular code path.
Thankfully repetitive language is less of a problem now that we have AI. The current syntax is just "add error handling to this function" :)
Writing it was never the problem if you're using proper tools, e.g. an actual IDE (IDEA does fine), or at least a snippet manager for your text editor. Inserting a wrapping error handling snippet requires two key presses.
It's reviewing mountains of that crap that's the problem, especially if there are non-trivial cases hidden in there, like returning the error when `err == nil` (mentioned by others in this thread).
You're getting downvoted, but this was what tptacek basically wrote about. Key points from his blog are that LLMs are good at tedium, and Go's structure is highly repetitive and amenable to LLM generation. The error handling tedium is probably part of why it's highly repetitive.
> I work mostly in Go. I’m confident the designers of the Go programming language didn’t set out to produce the most LLM-legible language in the industry. They succeeded nonetheless Go has just enough type safety, an extensive standard library, and a culture that prizes (often repetitive) idiom. LLMs kick ass generating it.
https://news.ycombinator.com/item?id=44163063 - 2386 comments
makes you miss the `or die` idiom :)
Love it, focus on building with what we have.
Resist enshittification, the greatest advantage in foundational software is sometimes saying no to new features.
Now that I think about it, are there any color schemes or extensions that highlight the error handling logic differently so that one can better focus on the “main” logic flow of the code while the error handling logic is still there?
You would have to infer the intent of the code. One example, from a small project I'm working on:
$ find . -name "*.go" | xargs grep 'if err !=' | wc -l
242
$ find . -name "*.go" | xargs grep 'if err ==' | wc -l
12
So about 5% of the error checking code is about handling the edge cases, where we are very much interested in what the error actually is, and need to handle those conditions carefully.If you discard that as "error handling noise", you're in for a bug. Which is, by the way, perhaps the worst side-effect of verbose, repetitive error handling.
Apropos syntax highlighting: many themes in regular use (certainly most of the defaults) choose a low-contrast color for the comments. The comments are often the most important part of the code.
Goland folds it visually partially https://github.com/golang/vscode-go/issues/2311
I would love something like this, and if it exists, I've not come across it. Offloading a way of differentiating error handling syntax vs. normal code to the IDE seems like a nice way of handling this issue.
This feels like it's making it worse lol
I love Go, but this is almost farcically hilarious:
> The goal of the proposal process is to reach general consensus about the outcome in a timely manner. If proposal review cannot identify a general consensus in the discussion of the issue on the issue tracker, the usual result is that the proposal is declined.
> None of the error handling proposals reached anything close to a consensus, so they were all declined.
> Should we proceed at all? We think not.
The disconnect here is of course that everyone has opinions and Google being design-by-committee can’t make progress on user-visible changes. Leaving the verbose error handling is not the end of the world, but there’s something here missing in the process. Don’t get me wrong, I love inaction as a default decision, but sometimes a decision is better than nothing. It reminds me of a groups when you can’t decide what to have for dinner – the best course of action isn’t to not eat at all, it’s to accept that everyone won’t be happy all the time, and take ownership of that unhappiness, if necessary, during the brief period of time when some people are upset.
I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go. IME these stylistic preferences are as easily abandoned as they are acquired.
To put another way: imagine if gofmt was launched today. It would be absolutely impossible to release through a consensus based process. Just tabs vs spaces would be 100 pages on the issue tracker of people willing to die on that hill. Yet, how many people complain about gofmt now that it’s already there? Even the biggest bike shedders enjoy it.
To take this analysis another level deeper: what has happened here is a classic example of bikeshedding — and, worse, fostering bikeshedding.
Everyone feels equipped to have an opinion about "what should be the syntax for an obvious bit of semantics." There's no expertise required to form such an opinion. And so there are as many opinions on offer as there are Go developers to give them.
Limit input on the subject to just e.g. the people capable of implementing the feature into the Go compiler, though, and a consensus would be reached quickly. Unlike drive-by opinion-havers, the language maintainers — people who have to actually continue to work with one-another (i.e. negotiate in an indefinite iterated prisoner's dilemma about how other language minutiae will work), are much more willing to give ground "this time" to just move on and get it working.
(Tangent: this "giving ground to get ground later" is commonly called "horse trading", but IMHO that paints it in a too-negative light. Horse trading is often the only reason anything gets done at all!)
> I love inaction as a default decision
The thing is, inaction is not simply "not taking an action"; Inaction is taking active action of accepting the current solution.
> I doubt that the best proposals are so horrible for some people that they’d hold a grudge and leave Go.
But people may leave go if they constantly avoid fixing any of problems with the language. The more time passes, the more unhappy people become with the language. It will be a death by a thousand cuts.
I love go. But their constant denial do fix obvious problems is tiring.
> But people may leave go if they constantly avoid fixing any of problems with the language.
For many people the current Go error handling just isn't a problem. Some even prefer it over the overengineered solutions in other languages. This brutalist simplicity is a core design feature I personally enjoy the most with Go, and forcing me to use some syntax sugar or new keywords would make it less enjoyable to use. I also don't think that generics were a net benefit, but at least I'm not forced to use them.
Go is a cemented language at this point, warts and all. People who don't like it likely won't come around if the issues they're so vocal about were fixed. It's not like there's a shortage of languages to choose from.
The article seems to admit that a `try` keyword restricted to statements / assignments only would combine the best parts and alleviate the major concerns of both the `try` and `?` proposals. It reads as though the concept has not been seriously discussed due simply to exhaustion.
Pro and con. It hinders progress, but it can also arrest the "enshittification" of a language that takes place when it slowly adds complexity and features and support for whole paradigms.
Python does offer a lot more utility for the expert these days, but it also went from the maxim of "There is one obvious way to do it" to having 5-6 ways to format strings, adding more things to be familiar with, causing refactoring churn as people chase the latest way to do it, etc.
I'm a C++ developer. I wouldn't want to go back to older versions of the language, but it's also very hard to recruit any newer programmers into using it willingly, and the sheer amount of stuff in it that exists concurrently is a big reason why.
I agree with you in principle, but this isn't like T-strings or C++ concepts. We're talking about a feature that has become infamous for it's lack of syntax in Go. A feature that every other major language has some syntax for.
Yeah this bugs me about the decision process too, but Go places more importance on backwards compatibility and stability than most other languages so it does align with their values. I'm not the biggest fan of Go but its nice having a language around that favors simplicity and stability.
> I love inaction as a default decision, but sometimes a decision is better than nothing. It reminds me of a groups when you can’t decide what to have for dinner – the best course of action isn’t to not eat at all, it’s to accept that everyone won’t be happy all the time, and take ownership of that unhappiness, if necessary, during the brief period of time when some people are upset.
Invalid comparison - eating one foodstuff or another affects a few people for a few hours. Significantly changing a popular language affects every single user of it forever.
But not changing has the same effect, also "forever" (and it also affects many non-users who could become users), so this is not a good argument for doing nothing
> sometimes a decision is better than nothing
Not in this case. The most popular Go proposal/issue of all times was 'leave "if err != nil" alone': https://github.com/golang/go/issues?q=is%3Aissue%20%20sort%3...
If go had started out having different syntax for error handling, would these same people request that it change to what go currently does? Or is this just resistance to change, and wanting to keep what they are used to?
I suspect it is the latter.
The second most popular issue was adding generics. So it's probably not a resistance to change.
It's true that Google is "design by committee" and consensus driven, but the Go team has been particularly obtuse about things, seemingly not understanding which opinions are valid and which are confused.
Most discussions of language features immediately fall into the politician's syllogism:
https://en.wikipedia.org/wiki/Politician%27s_syllogism
I appreciate the Go language's general sense of conservatism towards change. Even if you're not a fan of it, I think it's admirable that there is a project staking out a unique spot in the churn-vs-stability design space. There are plenty of other projects that churn as fast as they can, which also has its pros and cons, and it's great to be able to see the relative outcomes.
PS: it's kind of hilarious how the blog post is like "there are hundreds of proposals and miles of detailed analysis of these", vs the commenters here who are like "I thought about this for five minutes and I now have an idea that solve everything, let me tell you about it".
I'd happily come up with criticisms of any specific proposal and bikeshed it, but any one of these proposals would be preferable to the status quo.
I'd understand if they decided they needed more time to continue iterating on and analyzing proposals to find the right solution, but simply declaring that they'll just suspend the whole effort because they can't come to a consensus is rather infuriating.
“Simply declaring” is inaccurate description of the Go team’s decision. The team built several proposals, reviewed dozens more, and refined the process by gathering user feedback in multiple channels.
The Go team thoroughly explored the design space for seven years and did not find community consensus.
There are two possibilities.
1) There isn't consensus that improved syntax for error handling is needed in the first place. If that is the case, they should just say so, instead of obfuscating by focusing on the number of proposals and the length of the process.
2) There is consensus about a need for improved error handling syntax, but after seven years of proposals they haven't been able to find community consensus about the best way to add said syntax. That would mean that improved syntax for error handling is necessary, but the Go team is understandably hesitant to push forward and lock in a potentially inferior solution. If that is the case, then would be reason to continue working on improved syntax for error handling, so as to find the best solution even if it takes a while.
Sometimes doing nothing is the right thing to do. (Quote from Until Dawn)
Go chose not to change the error handling - Nature remained in balance.
I don't really understand this decision. They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue. Seeking the perfection of syntax that everyone agrees on seems to be the enemy of providing some good alternative to the status quo.
Their comment about providing some new syntax and people being forced to use it seems off base to me. It's nice to not have multiple ways of doing things, but having only 2 when it comes to error handling does not seem like a big deal. I imagine people will just use their preference, and a large percentage of people will have a less verbose option if they want it.
> They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue.
Agreement on a problem does not imply agreement on a solution.
It's not about perfection. It's about not having a solution that gets anywhere near a majority approval.
Let's say your neighborhood has an empty plot of land owned by the city that is currently a pile of broken chunks of concrete, trash, and tangled wire. It's easy to imagine that there is unanimous agreement by everyone in the neighborhood that something better should be placed there.
But the parents want a playground, the pet owners want a dog park, the homeless advocates want a shelter, the nature lovers want a forest, etc. None of them will agree to spend their tax dollars on a solution that is useless to them, so no solution wins even though they all want the problem solved.
Even if people in your example couldn't agree on a particular alternative, the outcome still is a less attractive area, maybe some will move out and fewer people move in. So, any solution would be better than the status quo - and they all would probably agree on that.
The lack of a good error handling story to a lot of people puts go in a mental trash bin of sorts. Similar (but different) reasons eg Java goes to a mental trash bin. I think leaving this issue unhandled will only make go look worse and worse in comparisons as the programming language landscape evolves. It might take 10 or 20 years but it'll always be unique in having "trash bin worthy" error handling. (this can perhaps be argued - maybe exceptions are worse, but at least they're standard).
> So, any solution would be better than the status quo - and they all would probably agree on that.
The point is that people do not agree that any solution is better than the status quo. In my analogy, if redeveloping that plot of land is quite expensive in tax dollars, people will prefer it be left alone completely so that money can be spent elsewhere than have it squandered on a "solution" that does nothing for them.
Likewise in Go, adding language features has a quite large cost in terms of cognitive load, decision load, implementation cost, etc. After many many surveys and discussions, it's clear that there is no consensus among the Go ecosystem that any error handling strategy is worth that cost.
In the analogy we might suppose everyone agrees that there is a problem and any solution is better than the status quo, but that's extremely unlikely in the case of Go. In my experience discussing this issue with Go users and critics, a lot of Go users find the status quo to be minimally chafing.
> The lack of a good error handling story to a lot of people puts go in a mental trash bin of sorts. ... It might take 10 or 20 years but it'll always be unique in having "trash bin worthy" error handling. (this can perhaps be argued - maybe exceptions are worse, but at least they're standard).
Remember that the context is syntactic error handling proposals, not proposals for error handling generally--the maintainers are saying they're only going to close syntax-only error handling proposals. While I have no doubt that there are lots of people who write of Go for its error handling syntax alone, I don't see any reason why a language community should prioritize the opinions of this group.
Additionally, while I have plenty of criticism for Go's error handling, I can't take "Go's error handling is 'trash bin worthy'" seriously. There are no languages that do error handling well (by which I mean, no implicit control flow and one obvious way to create errors with appropriate error context, no redundant context, clear error messages, etc). Go and Rust probably both give you the tools necessary to do error handling well, but there's no standard solution so you will have issues integrating different libraries (for example, different libraries will take different approaches to attaching error context, some might include stack traces and others won't, etc). It's a mess across the board, and verbosity is the least of my problems.
>It's about not having a solution that gets anywhere near a majority approval.
You'll never get it in any non-gamed environment.
In democratic voting in FPtP systems if there isn't a majority winner you'll take the top two and go to runoffs forcing those that are voting to pick the best of the bad choices.
This is the same thing that will typically happen in the city you're talking about, hence why most democracies are representative and not direct.
> They know from developer surveys that verbose and repetitive error handling is literally the number 1 issue.
According to 13% of respondents. So yes, it's the "#1 issue", but also not by a huge overwhelming majority or anything.
Honestly this is only because they made a bad survey. A ranked choice is better.
Lets say you have 5 choices. You give each choice a voting weight of 1 (not an issue) to (5 biggest issue). You only get to pick a weight once.
So in this type of voting even if everybody put error handling and #4 it could still win by a large margin if the 5 values were spread out over other concerns.
Python is a bit of a counter-example these days. I think they're in a good place right now, but it's hard to argue they've stuck to the premise of "There should be one-- and preferably only one --obvious way to do it."
- I need to do string interpolation: am I using f-strings or `string.format` or the modulo operator?
- I need to do something in a loop. Well, I can do that. But I could also just do a list or sequence comprehension... Or I could get fancy and combine the two!
And such and so-on, but these are the top examples.
Changing these things strictly adds cognitive load because you will eventually encounter libraries that use the other pattern if you're familiar with the one pattern. And at the limit of what this can do to a language, you get C++, where the spec exceeds the length of the Bible and many bits of it create undefined behavior when used together.
I think Go's project owners are very justifiably paranoid about the consequences of opening the Pandora's box on new features, even if it means users have to deal with some line-noise in the code.
Yeah, this is the single biggest reason I avoid go - I just don't want to clutter my "happy path" logic. It makes things harder to reason about.
"Errors are values", sure. Numbers are values and Lists are values. I use them differently, though.
I wonder if there could be "stupid" preprocessing step where I could unclutter go code, where you'd make a new token like "?=", that got replaced before compile time. For instance, "x ?= function.call();" would expand to "x, err := function.call(); if (err != nil) return err;"
There's no happy path in programming. Errors are just some of the many states in the code and the transition to them doesn't disappear magically because you chose to not specify them. Actually returning an error is just a choice, you can chose to handle the situation, maybe log the error and go on your merry way. Or you panic() and cancel the whole call stack back to a recover(). I like Go because it forces you to be explicit.
There absolutely is a happy path in programming - what you want the code to do, assuming no errors. It's the intent of the code and that surely is an important thing for the code to express.
> ...having only 2 when it comes to error handling does not seem like a big deal. I imagine people will just use their preference...
I foresee endless PR arguments about whether err != nil is the best practice or whatever alternative exists. Back-and-forth based on preference, PRs getting blocked since someone is using the "other" style, etc. Finally the org is tired of all this arguing and demands everyone settle on the one true style (which doesn't exist), and forces everyone to use that style. That is where the "forced to use it" comes from.
From the early days, Go has taken principled stands on matters like this, striving for simplicity and one way to do something. For example, `go fmt` cut through all the tabs vs. space nonsense by edict. "Gofmt's style is no one's favorite, yet gofmt is everyone's favorite."
> I don't really understand this decision.
The decision is disappointing, but understandable.
The blog post attempted to explain it, but it comes down to: A lot of energy has been expended without the community and the core team reaching any form of consensus. The current error handling mechanism has entrenched itself as idiomatic for a very long time now. And since the promising ones among the various proposals involve language changes, the core team, which is stretched already, isn't willing to commit to it at this time, especially given the impact.
This paragraph alone is fundamentally better than the page or so of text in the blog post.
I'm not sure what it is about the style of technical writing I've seen lately but just directly getting to the point versus trying to obfuscate the thesis on a potentially controversial topic is increasingly rare
The language was first designed without consensus and then released. It was used anyways.
But now a sort of democracy is required for changes. I’m not sure this is necessary.
The module system shows the Go core team will make unilateral changes if they feel like it. But if I read this correctly, there’s not a consensus even amongst the Go team on what to do here.
According to TFA, it's not quite "if they feel like it", it's when there isn't consensus among the community but (1) it's clear that there is a problem and (2) the Go architects can agree on a path forward.
In the module system case, there was a lot of consensus in the community, including widely-used and pretty mature tooling, and the core team surprised the community by implementing their own approach unilaterally. That predated the current rules of engagement. But in a similar situation today where the core team felt the community’s consensus was wrong, I wouldn’t be too surprised if that happened again.
> there was a lot of consensus in the community, including widely-used and pretty mature tooling
At the time the community was pretty fragmented between vanilla GOPATH, vendoring, godep, and one or two others that are escaping my memory. I don't think that meets my criteria for "a lot of consensus".
Probably a better example would be the type alias stuff that was introduced pretty explicitly to support Google's use case without much consultation from the wider community. That caused some kerfuffle as well; however, that also caused the maintainers to change their stance and lean into the community a lot more.
But yes, both of these examples predate any formal "rules of engagement" with the community and things have generally been better since. Moreover, these are the only two examples I can think of where the Go team pushed through some controversial, significant change. The Go team is extremely conservative (which is something I value, for the record), and far more likely to make no change at all even when there is a lot of enthusiasm for some particular change.
people that answer surveys don't represent all Go developers, many of us are fine with the error status quo
What I wonder about is the pool of potential Go developers. Is the error handling issue serious enough to stop developers from even considering Go? Go would have been an obviously better choice than most languages 30 years ago, but today there are many more good options.
If you shake things up so much that users who previous dismissed your language are interested, you might also be making a big enough change that your current users look around as well. The pool of prospective new language users is always large but they won’t join a language that is dying because it churned all its existing users and package maintainers.
I say this as someone that gets a very bad taste in my mouth when handling errors in go but use it a fair bit nonetheless.
If you're writing the universe, maybe. There aren't that many competitors when you take the ecosystem into consideration. It is the only reason I tolerate Go where it makes (some) sense — mostly CLI utilities that are too complicated for bash.
Every language has the potential to attract new developers if they change/add just this one thing.
> Is the error handling issue serious enough to stop developers from even considering Go
If it is, then I suspect those developers are going to have a thousand other non-overlapping reasons not to consider Go. It seems like a colossal waste of time to court these people compared with optimizing Go for the folks who already are using it or who reasonably might actually use it (for example, people who would really like to use Go, but it doesn't support their platform, or it doesn't meet some compliance criteria, or etc).
Ah, voting by survivorship bias.
No, this is not an example of survivorship bias. Here's the wiki link for your reference. https://en.wikipedia.org/wiki/Survivorship_bias
No this is an example of survivorship bias.
Let's say Go has such bad error handling that it becomes the number one reason people don't use it.
The people left that do use it will be the ones that don't care about error handling. Hence you're asking the people that don't care versus 90% of the audience you've already lost.
I don’t think you understood my comment. My argument was not “we should only care about Go users”, it was that we should not prioritize the opinions of people who may not even become users even if error handling is changed over the opinions of people who are very likely to use Go or those who already do.
Specifically, if Go’s error handling poses a constitutional objection for you, it’s probably just one item in a long list of things that prevent you from using the language. Changing everything to pacify you will take a long time and likely involve many breaking changes, and the end result is likely to be something that does not appeal to Go’s users or even many of the people who shared your objection about error handling but not all of your other objections.
This is not survivorship bias.
I've never used Go but having used other languages with exception-based error handling, I get why Go decided to go in a different direction... but reading this over...
Okay, so surely some syntactic sugar could make it more pleasant than the
if (err != nil) {
return nil, err
}
repeated boilerplate. Like, if that return is a tagged union you could do some kind of pattern matching?... oh, Go doesn't have sum-types. Or pattern matching.
Could you at least do some kind of generic error handler so I can call
y := Handle(MyFuncThatCanReturnError(x))
?... Okay, GoLang does not have tuples. Multiple returns must be handled separately.
Okay could I write some kind of higher-order function that handles it in a generic way? Like
y := InvokeWithErrorHandler(MyFuncThatCanReturnError, x)
?No? That's not an option either?
... why do you all do this to yourselves?
This doesn't actually makes the process simpler.
Error handling in Go is not just writing "if err != nil { return nil, err }" for every line. You are supposed to enrich the error to add more context to it. For example:
result, err := addTwoNumbers(a, b)
if err != nil {
return fmt.Errorf("addTwoNumbers(%d, %d) = %v", a, b, err)
}
This way you can enrich the error message and say what was passed to the function. If you try to abstract this logic with a "Handle" function, you'll just create a mess. You'll save yourself the time of writing an IF statement, but you'll need a bunch of arguments that will just make it harder to use.Not to mention, those helper functions don't account for cases where you don't just want to bubble up an error. What if you want to do more things, like log, emit metrics, clean up resources, and so on? How do you deal with that with the "Handle()" function?
Obviously I'm being terse for argument.
You can easily imagine
InvokeWithErrorLogger(fn, fnparam, log)
or InvokeWithErrorAnnotator(fn, fnparam, annotatorFn)
Or any other common error-handling pattern.Perhaps something like this?
result := InvokeWithErrorLogger(
func (err error) { // Error handler
incrementMetric("foo")
log.Error("bar")
},
addTwoNumbers, a, b,
)
But the problem is that this approach is not better than just writing this, which doesn't need any new fancy addition to the language: result, err := addTwoNumbers(a, b)
if err != nil {
incrementMetric("foo")
log.Error("bar")
return fmt.Errorf("addTwoNumbers(%d, %d) = %v", a, b, err)
}
Hence why all the proposals ended up dying with the lack of traction.Just to add my two cents—I’ve been writing Go professionally for about 10 years, and neither I nor any of my colleagues have had real issues with how Go handles errors.
Newcomers often push back on this aspect of the language (among other things), but in my experience, that usually fades as they get more familiar with Go’s philosophy and design choices.
As for the Go team’s decision process, I think it’s a good thing that the lack of consensus over a long period and many attempts can prompt them to formally define a position.
Yeah once you've been using it long enough for the Stockholm syndrome to set in, you come to terms with the hostile parts of the language.
I suspect a lot of us don’t have strong feelings either way and don’t find the verbosity “hostile”. No need for Stockholm syndrome if you don’t feel like a prisoner.
Of course you may have been joking, in which case “haha”. xD
If you say so. For me it's always been the opposite - I'm excited at the start about all the cool features, then slowly get disillusioned because of the warts.
This, it’s always the new people complaining about error handling.
I have many things to complain about for other languages that I’m sure are top-tier complaints too
I appreciate the argument that things can often be difficult for noobs but actually fine or better than alternatives once you get used to them.
But on the other hand, people who are "used to the way things are" are often the worst people to evaluate whether changes are beneficial. It seems like the new people are the ones that should be listened to most carefully.
I'm not saying the Go team was wrong in this decision, just that your heuristic isn't necessarily a good one.
This logic mostly only makes sense if your goal is primarily to grow the audience and widen the appeal, though. I think at this stage in the Go programming language's lifespan, that is no longer the goal. If anything, Go has probably started to saturate its own sweet spot in some ways and a lot of complaints reveal a difference in values more than they do opportunity for improvement.
To me, it makes sense for the Go team to focus on improving Go for the vast majority of its users over the opinions of people who don't like it that much in the first place. There's millions of lines of code written in Go and those are going to have to be maintained for many years. Of utmost priority in my mind is making Go code more correct (i.e. By adding tools that can make code more correct-by-construction or eliminate classes of errors. I didn't say concurrency safety, but... some form of correctness checking for code involving mutexes would be really nice, something like gVisor checklocks but better.)
And on that note, if I could pick something to prioritize to add to Go, it would probably be sum types with pattern matching. I don't think it is extremely likely that we will see those, since it's a massive language change that isn't exactly easy to reconcile with what's already here. (e.g. a `Result` type would naturally emerge from the existence of sum types. Maybe that's an improvement, but boy that is a non-trivial change.)
It’s fun, because when a newcomer joins a team, people tend to remind them that their bison is fresh and they might be seeing pain we got accustomed to. That’s usually said in a positive manner.
I'm intrigued as to whether "bison" here is a metaphor (for what?) or a cupertino (an error introduced by auto-correct or predictive text)
bison -> vision/point of view.
Bad auto-correct, my bad
I have a similar level of experience with Go, and I would go so far as to say it is in fact one of the best features of the language.
I wouldn’t be surprised that when the pro-exception-handling crowd eventually wins, it will lead to hard forks and severe fragmentation of the entire ecosystem.
To be honest, I really don't believe that will happen in the future. All of the proposals pretty much just add syntactical sugar, and even those have failed to gain consensus.
That's just survivorship bias isn't it? The newcomers who find Go's design and/or leadership obnoxious get a job that doesn't involve doing something that they dislike.
That's okay. Not everyone needs to like Go. Pleasing every programmer on the planet is an unreasonable thing to ask for. It's also impossible because some preferences conflict.
After over a decade of people bringing up the issue in almost every single thread about Go, it's time to give the language what it deserves: no more constructive feedback, snarky dismissals only.
Not infrequently by people who are not even Go programmers. And/or the same people who hijack every other Go thread to rant about how much they hate Go.
In a quick search, you seem to be one of them: https://news.ycombinator.com/item?id=41136266 https://news.ycombinator.com/item?id=40855396
You don't see me going around $languages_I_dislike threads slagging off the language, much less demanding features. Not saying anything is an option you know.
TLDR: we didn't fix it for such a long time that now it's too late to fix it. We won't be fixing it, thanks!
Edit: looking at the results of their H1 2024 survey, they're making a hard turn into AI, and most likely will be developing AI libraries to close the gap with Python.
Don't expect language features.
>For a while, the lack of generics surpassed complaints about error handling, but now that Go supports generics, error handling is back on top
>The Go team takes community feedback seriously
It feels like reading satire, but it's real.
They clearly are wrestling with these issues, which to me seems like taking the feedback seriously. Taking feedback seriously doesn’t imply you have to say yes to every request. That just gives you a design-by-committee junk drawer language. We already have enough of those, so personally I’m glad the Go team sticks to its guns and says no most of the time.
How is Go not a design-by-committee language? They don't have a single lead language developer or benevolent dictator, and as this blog demonstrates, they're very much driven by consensus.
True, but they’re very picky as committees go. But yeah, maybe not the best use of that expression…
Sigh. They failed.
Dudes, error handling is THE worst part of Go. All of it.
And not just the lack of the "?" operator. It's also the lack of stacktraces, the footguns with nils-that-are-not-nil, problems with wrapping, the leakage of internal error messages to users (without i18n).
Literally everything. And you're just shutting down even _discussions_ of this?
Hopefully this is the final nail in the coffin for all the “but I have to handle my errors” whining.
I don't know that there's whining about "having to handle errors" in principle, it's pretty clearly a complaint with the syntax and semantics of doing so
Some languages even make omitting error handling impossible! (e.g. Result sum types). None have anywhere near the amount of "whining" Go seems to attract
I’m somewhat surprised this was a topic that was deemed worthy of a blog post. It seems quite out of character with other blog posts on the Go blog which are usually about introducing a new feature. Sure, Go’s relatively verbose error handling has long been a source of controversy, but was it worth publishing a blog post just to say, effectively: “please shut up”?
The fundamental thing that try/catch and similar language structures give us is an error _boundary_. I feel that not having one is a weakness of the language. The nesting and syntax all serve the purpose of handling errors neatly. I think it works very well and Go has unsolved the problem.
I am not saying that the mechanism is perfect but it is more useful if we have it than not. IMO it's only weakness is that you never know if a new exception type is thrown by a nested function. This is a weakness for which we really don't have a solid solution - Java tried this with checked exceptions.
Go not using such a paradigm to me is bonkers. Practically every language has such a construct and how we use it best is pretty much convention these days.
I just wish the could stabilize the compiler before piling on more features.
I regularly run into internal compiler errors these days for pretty normal looking code.
It's getting to the point where I'm reluctant to invest more time in the language right now.
UPDATE: See comment below for full error message and a link to the code.
I've never seen a single internal compiler issue in 10 years of working in Go.
What error are you talking about?
Not OP, but I found a compiler bug in the initial release of 1.18, though it was also quickly fixed: https://github.com/golang/go/issues/51733
In that case, you might enjoy this part of the article:
> For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling
If you do stuff the language doesn’t support, it’s not the languages problem.
I did nothing weird, played around a bit with the new iterators.
Internal compiler errors are very much the implementation's problem.
UPDATE: See comment below for full error message and a link to the code.
I only say this because I have yet to encounter a single internal compiler error in years of writing Go professionally. I can’t conceive of the kind of code one must be writing for internal compiler errors to be a repeated issue.
So here's the complete error message:
<unknown line number>: internal compiler error: unexpected types2.Invalid
Please file a bug report including a short program that triggers the error. https://go.dev/issue/new
And here's the code that triggers it:
https://github.com/codr7/shi-go/blob/main/src/shi/call.go
The code is never referenced in the project, but running make in the project root with this file in it triggers the error. Remove the file and the error disappears.
Happy now?
Paste exactly what commands you run and exactly their output, including exactly the rev of the repo you're running them in.
> Remove the file and the error disappears
Remove the file and the code no longer compiles, because the file contains definitions that are used by other code in the package. If removing that file doesn't break your build, something is wrong with your build!
Your Makefile seems to be calling `go test src/tests/*` which is invalid syntax, I suspect that's just one of many similar kinds of mistakes, and likely indicative of a misunderstanding of the language tooling...
> https://github.com/codr7/shi-go/blob/main/src/shi/call.go
This code is buggy from tip to tail, my goodness! Starting with no `gofmt` formatting, everything in https://github.com/codr7/shi-go/blob/main/src/shi/vm.go, invalid assumptions in everything that embeds a Deque, no meaningful tests, misuse of globals, the list goes on and on... ! It seems like you're programming against a language spec that you've invented yourself, maybe influenced by Go, but certainly not Go as defined!