I think the try syntax was the most promising of the proposals if it were just syntactic sugar for the common pattern of returning unhandled errors.
Condensing
foo, err := bar
if err != nil {
return err
}
into one line
foo := try bar()
wouldn't replace the old syntax, it would just be ergonomic.
Personally, coming from C, I don't mind the current syntax. As Robert said, it's one thing to write code, and another thing to read it. When reading Go code, its explicit, unsweetened syntax is generally a boon.
But I really don't see the downside here. You wouldn't even have to use the new syntax if you didn't want to.
I'm the creator of the Odin programming language and Odin does share a lot of similarities with Go; multiple return values being a big one. One thing I experimented with a few years ago was this try that you propose (since it was obvious from the get go). What I found is that:
try was a confusing name for what the semantics were
try as a prefix was the wrong place
What I found is that just changing the keyword and its placement solved a bunch of problems. It was surprising to me it was purely a syntax problem. It became or_return.
foo := bar() or_return
Most people were confused regarding the semantics of try, but as soon as I tried or_return, it became obvious.
I'd also argue that or_return is also not hiding any control flow now because it literally says what it does in the name.
The article I wrote on it at the time: https://www.gingerbill.org/article/2021/09/06/value-propagation-experiment-part-2/
However, Odin has more than justor_return, it has or_break, or_continue, and or_else.
or_return on its own is not enough to be useful, and you do need the whole family. (And before people suggest I should have just added the or keyword and then have or return etc, I did experiment with that and it was actually a very bad idea in practice for non-obvious reasons).
But as I say in that article, my general hypothesis appears to be still correct regarding error handling. Go's problem was never the if err != nil aspect but the return err.
The most important one is the degenerate state issue, where all values can degenerate to a single type. It appears that you and many others pretty much only want to know if there was an error value or not and then pass that up the stack, writing your code as if it was purely the happy path and then handling any error value. Contrasting with Go, Go a built-in concept of an error interface, and all error values degenerate to this interface. In practice from what I have seen of many Go programmers, most people just don’t handle error values and just pass them up the stack to a very high place and then pretty much handle any error state as they are all the same degenerate value: “error or not”. This is now equivalent to a fancy boolean.
Go lacks a rich type system and the error system exacerbates this problem. This is why Odin doesn't have a degenerate type for errors where all errors collapse to it. In Odin, errors are just values, and not something special. I have found that having an error value type defined per package is absolutely fine (and ergonomic too), and minimizes, but cannot remove, the problem value propagation across library boundaries. Go does treat errors as values but the error interface is in practice a fancy boolean with a string payload and people don't handle the errors, they just pass them up the stack (which Griesemer states in the article).
Go not having enums and tagged unions (and them being separate too rather than unified like ML-style unions, i.e. Rust) from the start is what Go needed but sadly doesn't have. I understand unions didn't exist because of their conflict with interface, but I do wonder if that's because they were trying to implement another kind of union rather than one that would work well with Go.
P.S. I do agree with the decision to put all of this on hold for Go. Go's slower approach to implementing new constructs is a good thing in my opinion, even if it annoys people.
Wow thanks for the response! I didn't expect you of all people to respond to my random reddit comment.
I really like those error handling semantics. I was actually looking at Odin a while back, but became interested in Zig. I'll have to look more deeply at the language now.
At the moment I'm sticking to C and Go because every C replacement (e.g. Zig) is still pre-1.0 and frequent breaking changes are a dealbreaker for me. And I really don't like Rust.
To me the standout features of Zig were:
Explicit allocators. It's very clear exactly what is allocating memory, and where, because you have to pass the allocator to the function.
Optionals enforce null handling at compile time, and error unions enforce error handling
Defer makes cleanup simple and explicit
Granular compile time and runtime safety checks for different builds
Integrated, robust tooling: basically having gcc, make, valgrind, static analysis, unit testing (with perf), etc. all in one place.
Comptime
Incremental compilation and seamless cross compilation
No hidden control flow
Granular stdlib
No UB by default
Seamless C interop
I'd actually be using Zig right now if it weren't for the lack of proper async and the language instability. One thing about Go that is hard to give up is its concurrency model.
I actually think it could be translated properly to a language like Zig or Odin with a little creativity but it would have to be optional.
I've thought about it. It would be something like:
Lightweight, user-level threads (fibers) with an adaptive manual yield
Bidirectional typed channels (conduits) for thread safe communication between fibers, passed as send-only or receive-only to fibers
A lightweight work-stealing scheduler that coordinates and multiplexes fibers across OS threads (internal deque to hold fibers; each OS thread runs an event loop to handle the fibers the scheduler gives it).
You'd import and initialize the scheduler. Then you could add fibers to it and start it. But if you didn't want/need all this you could just use basic concurrency primitives.
I don't know enough about Odin to compare, for now I'm stuck with C and Go. It's not a bad place to be, but I know we can do better.
Odin has explicit allocators and context.allocator. Explicit allocators everywhere are a double-edged sword because most people never want nor need them, and they will be come lazy and just rely on global-ish allocators and then pass them around. I know this because even I was doing this when I was using custom allocators in my C code, well before Odin or Zig existed. Odin's implicit context system exists precisely because I know most people will not use custom allocators (or many other things), and thus this allows you to intercept that code quite easily.
n.b. pretty much every procedure in Odin that allocates memory does allow you to explicitly set the allocator you want.
Maybe(T) is a thing in Odin. In fact it is just defined as the following:
Maybe :: union($T: typeid) {T}
And allows for things like Maybe(^T) being the same size as a pointer
defer exists in Odin
Odin has numerous runtime safety checks too which make sense for the semantics of the language.
The Zig compiler bundles with Clang. Odin's doesn't. That is a choice for numerous reasons too.
Odin doesn't have compile time execution like Zig by design. It's something I decided against in the end because of numerous reasons I won't get into here. Odin has many other concepts which aid the problems that Zig uses comptime to solve.
(a) Zig might have some incremental compilation, but I'd argue Zig is a much slower compiler than Odin regardless. Odin will eventually get such features, but it's not the highest of priorities yet because of how complicated it is to do correctly.
(b) Cross-compilation is not actually seamless even in Zig, because of what you have to do to correctly target things. If you want to cross-compile to Windows, you're going to be in for a lot of fun because Zig assumes the MinGW toolchain. If you can get away with that, then you might be fine, however most precompiled libraries and tooling assuming the MSVC toolchain, and it is not compatible in practice. The C APIs are meant to be ABI compatible but in practice they rarely work well because many are just C API facing and rely on loads of C++ code and many other hacks.
Cross-compilation is just a nightmare in general if you actually want to support things correctly. I'd argue most people who ask for it usually (not everyone though) are doing it because they don't have the target platform to test on, and literally just want to build and not test on the platform. It's literally hoping it works, or worse... let their customers be the "testers".
This "no hidden control flow" is kind of a misnomer because it depends on whether or not you think try is hidden control flow. If you don't, then Zig and Odin don't any hidden control flow. If it is, then Zig and Odin have hidden control flow.
I am not sure what granular means, but you only use what you use in Odin.
Zig has loads of UB by default, and even used to flaunt it on its website as being a "good" thing. Odin strives to minimize the amount of Undefined Behaviour as possible whilst still having to have the Unobservable Behaviour due to having a memory model that is compatible with C.
Odin has seamless C interop with its foreign import system, and I'd argue it is much much nicer than Zig's too. It allows you to define the linking stuff directly in the source code meaning you don't need a separate build system.
As for you latter thing, "fibers" as you describe them are anything but lightweight to do in the way you describe. Sadly, to do what Go does, you need to have an extremely heavy runtime which is effectively sandboxed from third party code. This is why CGo is effectively a separate language. If a language is to have green-threads, which is what you are describing, you need to design the entire language and runtime around them from the ground up, and effectively have something akin to GC (or another form of automatic memory management).
Thanks for the detailed response! I appreciate it. I'm very interested in languages like Odin in general. I guess my only thing is I'm not sure when Odin will be stable. The versioning is a little unclear. But after 1.0 I'd be happy to try it out!
I think you can get around GC with green threads actually. You just need a contract system, but it does require a special kind of scheduler. That's something I'll get to one day when I write a library to demonstrate. I have too many other things on my plate at the moment.
Odin the language (not the core library) is effectively done. We have resorted to monthly releases so that we have a regular release schedule rather than sporadic releases which use the x.y approach.
As for green-threads, you still need to design EVERYTHING around it. Memory management (e.g. GC) is the least of the concerns. As soon as you need to interact with the OS or third party code, you are kind of screwed.
And yes, I know you have to design a lot around green threads, but OS threads are very heavy and don't scale well for I/O bound tasks. It does make sense for languages like Odin and Zig to delegate that to external libraries though.
libdill (https://github.com/sustrik/libdill) is pretty close to what I'm talking about here. You don't necessarily have to have a heavy runtime, libdill is very lightweight and outperforms the traditional threading model for I/O bound tasks by orders of magnitude.
Go does treat errors as values but the error interface is in practice a fancy boolean with a string payload
Tell me you don't understand interfaces without telling me you don't understand interfaces. lol
Errors can be any value which has a String() method. Therefore errors in Go can be any value and can contain any amount of information. Also, errors can be wrapped/unwrapped to provide context as they cross API boundaries.
Ken Thompson, Rob Pike and Robert Griesemer got it all wrong hey??? .....Okay. 👍
I understand interfaces very well. You don't seem to understand what I am saying how error is treated in practice, and you've just demonstrated that you've not understood what I wrote.
in practice a fancy boolean with a string payload
That "string payload" is the String() method. And the "fancy boolean" aspect is because most people rarely handle the error and just do err != nil. And because it can be ANYTHING, that's what I mean by the a "degenerate type".
I know errors can be wrapped/unwrapped, even switched on too, but people rarely do anything with that because of how they use the error interface.
The downsides are explained in the linked article. The one I immediately thought of was that it makes it impossible to wrap an error, so if you get an error “EOF” it’s hard to know where it’s coming from if it’s not wrapped like “request failed: error reading stream: EOF”.
The authors brought up some other interesting points.
Not necessarily defending the status quo, I’d love a more ergonomic solution too, but I can understand the downsides.
The try syntax would just be sugar to propagate the unhandled error to the caller. If you wanted to handle and augment the error, you'd just do
foo, err := bar()
if err == BAZ {
return fmt.Errorf("more detail goes here %v", err)
}
So unless the argument is that err should never be propagated to the caller unaugmented, I think the try syntax (as described above) wouldn't detract from the language.
Yeah I understand that. My argument is that it is almost always helpful to augment an error (even though I wouldn’t go as far to say “never be propagated unaugmented”). The ? syntax disincentivizes augmenting the error. For sure it’s optional, but if it doesn’t allow doing what many would consider best practice, then it’s not helpful.
The article had another counter-arguments btw, including the ability to set breakpoints etc
Yeah that's fair enough. Certainly anything that wouldn't allow best practices would be a bad change, but if you wanted to add breakpoints you could just remove the sugar.
I think the argument for discouraging bad practices is the most compelling one.
TL;DR: they considered several proposals, put out a couple of their own, but too many people chimed in and complained – even about the ? borrowed from Rust that so many seem to laude as the solution. And so now they have chosen to do nothing about it instead of continuing to try to please everyone. They'll revisit at some point in the future.
Go has a great feature set wrapped in a fundamentally flawed language. I suspect its popularity is temporary, and another, better language will eventually displace it (even if just another language on the same runtime).
I think it only even got to where it is now by chance through the Google Borg to k8s to DevOps vector.
If a FAANG had developed something like Crystal instead and used it in a transformative project like Borg, then I think we’d all be in a much happier place now.
This is every language, though. Go has already had a pretty great run compared to most. I suspect a lot of that is that the people who like it like it because it's little more than a more plesant C that's dead simple to deploy. There's plenty of other amazingly feature rich languages out there to choose from, Go doesn't need to be something it isn't.
Agreed, but I think there’s still an opening at the app tier for a better solution. Go, Java, PHP, C#, Python, Ruby, Typescript, Rust, etc all have some deficiencies there in my opinion.
Go has been successful because it's so absurdly simple any idiot can pick it up and be productive in a weekend. All the Go haters want to tack on a bunch of complex features that would make the language no longer idiot proof, and they get really, really (unhealthily) upset that the designers don't want that.
I work with go professionally. Go is definitely not idiot proof. There are so many footguns in go, there's books written on it. It is easy to pick up, but not easy to master and very trivial to make many many mistakes.
I'm exclusively using idiot proof to explain the ability to quickly and easily read and write the minimal language features. Go has resisted the perl-ification so many languages go through, adding syntactic sugar to do the same thing 6 different ways (e.g. modern C#).
Code quality hasn't much to do with being an idiot, some of the absolute worst bloated and incomprehensible code I've ever seen has come from some of the smartest developers I know. Usually the kind of abstraction and indirection that evolved out of the phrase "woudn't it be cool if...".
I'm surprised one of them hasn't done this. Every thread about Go shows there is a huge desire for a natively compiled language that is more expressive. It just seems like none of the big players are trying to make it happen.
The closest I can think of right now might be Swift but it doesn't seem like Apple is going to invest enough to make it usable outside of its ecosystem. C#, Java, Kotlin are all stuck with a big language runtime and the other languages like D, Crystal, or Nim don't have a large backer to gain popularity.
it doesn't seem like Apple is going to invest enough to make it usable outside of its ecosystem
I wonder.
They just relaunched swift.org, and if you go to Install, there are Linux and Windows tabs, with the Windows one even using the new winget package manager.
Probably largely community-driven, but I wouldn't rule out that it's eventually of interest to Apple, too — they do have Windows companion apps, at least (e.g. https://apps.microsoft.com/detail/9np83lwlpz9k?hl=en-us&gl=US), and I imagine they would like if those could share more Swift code with their macOS/iOS counterparts.
There are people who have done it. V was supposed to be a better go. That's the biggest one I can think of but I remember many posted on /r/ProgrammingLanguages like borgo, pipefish, etc.
I honestly love working with Swift. But I don’t do Apple development anymore, and the support for cross-platform work just isn’t there. Vapor is not bad for a web backend, but it’s not enough.
Golang started in the pre-2010s Google which had a much different culture and ideas about funding. Compare that to nowadays where Google has killed several internal and external language and framework projects with layoffs along with it.
The big companies have all this sway and technical expertise but can't invest in great community projects anymore because they are beholden to corporate greed. Even Google 20% projects have become more focused on making money - with developers focusing more on things with easier metrics for their promo packets rather than actually cool things.
It is used in real, production software (e.g. https://lavinmq.com) but I don’t think it’s common. I just recently discovered it myself, but I’ve started using it for some things (nothing big yet, but in a business context).
Absolutely. Certain parts like the compilation and the concurrency/async are perfect. Other parts are just shit. Another language should come along, take the genius parts, leave the rest, and end up with something great.
The fundamental flaw with go is that the designers think programming languages should be a hindrance to creating abstractions. This goes counter to most languages where abstraction is the goal.
I could make a huge list of specific flaws but I am sure they have all been mentioned already by others. Some decisions they made just seem obstinate and purposefully cruel like lack of named parameters and default values in function parameters and the inability to specify defaults in structs, lack of union types etc.
All of these require developers to do backflips in order to accomplish mundane ordinary tasks other languages can handle with ease.
I should also add that it makes dealing with databases a HUGE pain in the ass. They even put an sql package with what kind of looks like result types in the sql package because people were just using pointers and I guess they didn't like that.
I never said anything about current adoption I said what is actually discussed. Nobody looks to go language looking for ideas or solutions, the innovation is happening in Rust.
Yeah yeah sure. I work with rust and the bullshit of this language is intolerable. Good if you come from C/C++ but dog*hit of you come from other decent languages
Another important part is that the vast majority of "solutions" provided, focused primarily on syntactic sugar for the default "passthrough" error handling, which seems like an important issue, until one becomes fluent enough in Go for the eye to simply skip
if err != nil {
return err
}
While it may have been nice to have that in grom the start, if such schemes were introduced now, they would go againstvone of the core design goals of go, that tgere should be one, and preferably only one obvious way how to do something.
Honestly this is a rock solid idea - go makes a great IL, it lacks the expressiveness and type system necessary to build maintainable software at scale, but with a few improvements sugarred over it...
While it does make for a great IL being minimal and all, you cant possibly say with a straight face that it lacks features for being used at scale if you take a brief look at reality.
I never found the error handling to be that onerous. I like that it's explicit, and while it does add verbosity, any modern IDE can be configured to auto-complete it for you while writing or collapse it while reading.
The verbosity is actually less than try/catch if you error check every errorable function call. Of course most people don't check every function call when using a language that supports try/catch, but I'm not sure if that is really an advantage, as forcing you to consider the possible error conditions seems more like a feature than a bug.
This article gave me blue balls. I read with anticipation. It's finally happening. The day many thought would never happen has finally come. Just like generics. Reading every proposal that was shot down. Wondering what kind of crazy solution they came up with...
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.
I'm probably in a minority, but I like handling errors as values precisely because of the above. Go's error handling encourages you to handle each potential error appropriately and allows you to add detailed context before returning the error. At 4am when you've been called out because prod's down, that is more valuable than stack trace IME. Doing the same with eg Java exceptions would require each call to be wrapped in a try/catch and, voila, you have just as much boilerplate.
I quite liked the Rust-ish ? option mentioned in the article, but again you're missing the context part of that feature which is it's most valuable asset.
Without any of that, error-as-values is perfectly workable. For me. YMMV ;)
I agree that context is important. In Rust, there is the popular anyhow crate which allows you to do that:
use anyhow::{Context, Result};
fn main() -> Result<()> {
...
it.detach().context("Failed to detach the important thing")?;
let content = std::fs::read(path)
.with_context(|| format!("Failed to read instrs from {}", path))?;
...
}
It's a shame that, for all the nice methods and syntactic sugar rust has for Result and Option, we have to resort to a crate to do the right thing (add context to an error when returning it).
Rust intentionally has a really small standard library, that is more concerned with building a common vocabulary and primitives to keep external crates compatible with each other.
Most higher level languages with batteries included standard libraries make a lot of assumptions that just aren’t acceptable for systems programming.
For instance, something even as simple has error handling is going to be different depending on if you are writing for an embedded device, a web server, or a CLI tool.
Anyhow isn’t acceptable for a lot of the use cases and environments that rust is designed for. It’s just a fact of life that most rust projects will have to pull in external crates.
I think it's because anyhow is not a zero-cost abstraction. Anyhow errors allocate to the heap because it's a dyanamic, non-fixed sized struct. The most optimal way would be to create your own error structs/enums and implement the Error trait but it gets tedious very quickly.
It kind of is, but at the same time I don't see how the language itself could solve this. Attaching context to an existing error requires either generic soup (which ruins ergonomics), or heap allocations meaning it can't work on #[no_std].
Adding an anyhow-like catchall error type with context to the standard library would also likely result in more libraries returning that instead of "proper" error enums, which would be a shame.
I would note that Rust & Go have different values, leading to different trade-offs. In particular Rust holds up uncompromising performance as an important value.
In Go, there's no reason that an error could get a backtrace, or that ? could add a frame to the error's backtrace incrementally, which while not "context" would give a lot more information than... nothing, the default in Rust.
the safe APIs are complemented by unsafe APIs so the final users can pick whether they want safe or fast
The average developer doesn't have that choice. He will get both a reliable and fast solution.
You simply don't understand the concept of unsafe.
Rust safety is based on clear invariants such as "a pointer always points to existing data of the corresponding type". The compiler guarantees these invariants. When writing abstractions with unsafe, the compiler guarantees are transferred to the developer - he must make sure that the invariants are fulfilled.
But back to the topic of error handling - I meant that you are wrong in that Rust chooses speed of execution over correct error handling with added context. No, Rust allows you to add context to errors and does it much better than that.
Perhaps it's my C background showing but I like the fact that there is no easy way out of it. In fact, if someone tells you to learn to ignore it, I think they simply don't get the design principles of Go.
Error handling should not be an afterthought in applications designed for resilience, which is what I understand Go was intended for. Much of the discussion I've seen about this issue was in the vein of "I don't want to be bothered with it". Well, I do. When I don't, I use Python or something. Haven't tried Rust yet.
You shouldn't ignore error conditions and I like that Go forces you to think about them. I spend noticeably less time debugging weird stuff in Go than in other languages; there's a significantly higher chance that code which compiles is correct.
That's not the same thing as saying that it could use some syntax to make you think about error handling in a less verbose way though.
Depends on perspective. There is absolutely easy way out of it, you can just .... not do any error checking at all. Simply do:
_ = os.Remove("/file/doesnt/exist")
And no one is going to force you to handle error if you don't want to. But when you want to do the right thing, suddenly your code becomes fucking ugly, that's like punishing good behaviour.
Error handling should not be an afterthought in applications designed for resilience
Exactly which is why neither Go nor C is doing enough. In Rust, first of all the compiler would force you to handle things that can fail (Option/Result). And then it gives you syntactic sugar that makes handling errors way easier on the eye. That's strictly superior in both ways.
I think you'd like the way Rust does it. It forces you to check whether or not an error occurred before you can access the value at all, you don't really have the option not to bother with it. At the same time ergonomics are much nicer because of some syntactic sugar and helper functions to transform error values.
Where that Errorf format needs to be different depending on what was happening or what went wrong.
But then the call below it usually has has the same or similar boilerplate, and the call below it should too. It collects the errors as needed, building up the path. But, only if every piece of the code in the stack uses that pattern. If any piece partway through forgets to do it, then the information is lost. It's relying on convention that has evolved over time instead of a built in design.
So we just have to do the explicit thing. I think it's OK. I know lots of people hate it.
In the rust ecosystem they have a kind of alternative problem: there's syntactic sugar for returning errors, but adding context is fiddly (often), so it's normal to use a third party crate (anyhow) for it.
At least in go, people mostly just use what the language provided.
Error handling should be 3/4 of your code in any moderately complex app. Robust production software needs to handle all kinds of errors gracefully. Errors are values and you can handle them (or not) however you'd like.
You shouldn't "just learn to ignore" error handling, it's part of the control flow.
or some variant of this is the majority of what's done, since errors can't really be handled in place in a lot of cases. Maybe some additional info can be added to it, such as wrapping the error in another error. At least that add some value to the code.
You shouldn't "just learn to ignore" error handling, it's part of the control flow.
Agreed. That's why the stance from many in the community of "just learn to ignore it" is so awful.
Such an embarrassingly managed language. It's consistently behind the times on basic, fundamental features and when given an opportunity to fix something that's widely hated they decide to do nothing rather than deal with some grumbling from a subset of people until they get used to it.
Just adopt something. ? from Rust is widely accepted as a reasonable solution. If you don't want to worry about it conflicting with ternaries or something, use a built-in macro like the try "function". Doing nothing is the worst possible choice
For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.
They've decided to actively suppress discussion around the idea.
Honestly I think that’s a good move, depending on how long they do it. This blog post has already sparked dozens of suggestions on reddit and hacker news about how to resolve the issue. I’m betting they saw that coming and given the seven years of attempts to handle this decided they didn’t want to be messing with all the suggestions the announcement would spawn.
Let's face it, even if they picked an imperfect solution that many bemoan today isn't the ideal solution, in a few years time, most of the complainers would, pragmatically, adopt the solution regardless, and most complaints would die down.
Instead, by doing nothing, they stand to suffer the constant stream of complaints forever.
Now, that Go has generics, wouldn't it make sense to implement Result type and, perhaps, some syntactic sugar for it. That will not help the standard library - compatibility promise does not allow it. And any new code, including new methods and/or packages in the standard library could use Result.
Go doesn't have tagged unions to match on like Rust does (with enums). So a Result type in Go doesn't really improve things over a simple (value,error) tuple.
Also, Result in Rust is convenient because there is syntax sugar for it. If you don't have sugar, it's worse than what Go currently has (match every Result to unwrap; No ?, no let-else.. remember that?)
Also also, everything uses Result. If you don't have that, error handling is different depending on what library you use, or even which function you call. That is terrible for many reasons, including having to cast between different kinds of errors
"Adding" a Result type to a language later on doesn't really make sense. And the same is true for many other functional paradigms
I understand and agree in general but hear me out on a few things.
First, tagged unions are implementation details. So long as the type implements a specific interface, it should not matter. That is where syntactic sugar comes in. And I suggested that they add that sugar in my original message.
Not everything in Rust uses Result. When one implements bindings over C ABI, one has to wrap functions that return regular error codes with Rust functions that return Result.
Perhaps, as a typical Rust community approach, they let developers come up with different implementations of Result-like types in the wild and then decide on recommended way of doing it.
They could make it so that the hypothetical Result type could be destructured into the classical/old res, err form (with one of them nil) to enable backward compatibility.
They still would need some correspondence for Into<Error> though, I guess, to really match rust on this.
I'm the creator of the Odin programming language and Odin does share a lot of similarities with Go; multiple return values being a big one. One thing I experimented with a few years ago try that you propose (since it was obvious from the get go). What I found is that:
try was a confusing name for what the semantics were
try as a prefix was the wrong place
What I found is that just changing the keyword and its placement solved a bunch of problems. It was surprising to me it was purely a syntax problem. It became or_return.
foo := bar() or_return
Most people were confused regarding the semantics of try, but as soon as I tried or_return, it became obvious.
n.b. I know ? was also suggested but it's not exactly obvious when scanning code. And most of the time when people say the word "read code", they mean "scan code". And a single sigil like ? is really easy to miss.
I'd also argue that or_return is also not hiding any control flow now because it literally says what it does in the name.
The article I wrote on it at the time: https://www.gingerbill.org/article/2021/09/06/value-propagation-experiment-part-2/
However, Odin has more than justor_return, it has or_break, or_continue, and or_else.
or_return on its own is not enough to be useful, and you do need the whole family. (And before people suggest I should have just added the or keyword and then have or return etc, I did experiment with that and it was actually a very bad idea in practice for non-obvious reasons).
But as I say in that article, my general hypothesis appears to be still correct regarding error handling. Go's problem was never the if err != nil aspect but the return err.
The most important one is the degenerate state issue, where all values can degenerate to a single type. It appears that you and many others pretty much only want to know if there was an error value or not and then pass that up the stack, writing your code as if it was purely the happy path and then handling any error value. Contrasting with Go, Go a built-in concept of an error interface, and all error values degenerate to this interface. In practice from what I have seen of many Go programmers, most people just don’t handle error values and just pass them up the stack to a very high place and then pretty much handle any error state as they are all the same degenerate value: “error or not”. This is now equivalent to a fancy boolean.
Go lacks a rich type system and the error system exacerbates this problem. This is why Odin doesn't have a degenerate type for errors where all errors collapse to it. In Odin, errors are just values, and not something special. I have found that having an error value type defined per package is absolutely fine (and ergonomic too), and minimizes, but cannot remove, the problem value propagation across library boundaries. Go does treat errors as values but the error interface is in practice a fancy boolean with a string payload and people don't handle the errors, they just pass them up the stack (which Griesemer states in the article).
Go not having enums and tagged unions (and them being separate too rather than unified like ML-style unions, i.e. Rust) from the start is what Go needed but sadly doesn't have. I understand unions didn't exist because of their conflict with interface, but I do wonder if that's because they were trying to implement another kind of union rather than one that would work well with Go.
P.S. I do agree with the decision to put all of this on hold for Go. Go's slower approach to implementing new constructs is a good thing in my opinion, even if it annoys people.
I mainly write C#, but I find Swift's approach interesting:
functions that throw need to be declared with throws
you can consume them with try, but if you do so, your function, too, needs throws
or, you wrap that in a do/catch, which I bet is familiar to people
or, you use try?, which means: if the function fails, just assign nil instead
or, finally, if you're really sure there's no danger, you use try! — the runtime crashes if an error does occur
IOW, this:
func someThrowingFunction() throws -> Int {
// ...
}
let y: Int?
do {
y = try someThrowingFunction()
} catch {
y = nil
}
Can be shortened to this:
func someThrowingFunction() throws -> Int {
// ...
}
let x = try? someThrowingFunction()
I love that, because "just make it null if the method errors out" is a very frequent scenario in C#.
If .NET/C# were redesigned today, I would love if the Try-Parse pattern (returning a bool, with the real value in an out param) were eschewed in favor of try? syntax.
The test if err != nil can be so pervasive that it drowns out the rest of the code.
It's barely better than VB Classic's On Error Resume Next. Just… no thank you.
Personally, I'm happy if they do nothing because to me none of the proposals make sense. If there is an error you have to deal with it, and I'd rather have some boilerplate for returning it explicitly than some syntactic sugar for hidden control flow that really just passes the error to the next function anyway, let alone compound option types that do the same in an even more obscure way. I've been programming solely in exception-based languages before I came to Go, and they had no advantage. People just feel they have advantages when they ignore invalid states and errors and handle them too late, often in some kind of catch all clause that makes no sense.
Error handling in Go makes no sense, here is my comment on this:
Errors can be expected (when you know something might go wrong) and unexpected (exceptions or panics).
Errors can contain useful information for the code (which it can use to correct the control flow), for the user (which will tell him what went wrong), and for the programmer (when this information is logged and later analyzed by the developer).
Errors in go are not good for analysis in code. Although you can use error Is/As - it will not be reliable, because the called side can remove some types or add new ones in the future.
Errors in go are not good for users because the underlying error occurs in a deep module that should not know about e.g. localization, or what it is used for at all.
Errors in go are good for logging... Or not? In fact, you have to manually describe your stack trace, but instead of a file/line/column you will get a manually described problem. And I'm not sure it's better.
So why is it better than exceptions? Well, errors in go are expected. But in my opinion, handling them doesn't provide significant benefits and "errors are values" is not entirely honest.
Java is the only "classic" language where they tried to make errors expected via checked exceptions. And for the most part, this attempt failed.
I really like Rust's error handling. Because the error type is in the function signature, errors are expected. With static typing, I can explicitly check for errors to control flow, which makes the error useful to the code, or turn a low-level error into a useful message for the user. Or just log it. Also, if in the future the error type changes or some variants are added or removed, the compiler will warn me.
Article does eventually get to the issue that most errors aren't really handled anyway. Most cases more just propagate errors than actually handle them. But then the article goes on to act like adding stack traces is part of handling an error. I can't think of the last time that was true. Stack traces are used to flesh out bug reports really. They are used to log errors. It is in theory possible to use the trace as part of the handling, and handle one error different than another based upon the trace (execution state) when it happened. But I've never see it done, and I would suggest instead you would create a new type of error code when you recognize the particular failure, instead of trying to divine what certain configurations of stack traces indicate.
Most of these shortcuts given really are leaning into error logging or possibly propagation as the way that errors are dealt with. And sadly, I think they are often correct. That's most of the principle that exceptions are built on too, isn't it?
SIeeplessKnight@reddit
I think the
try
syntax was the most promising of the proposals if it were just syntactic sugar for the common pattern of returning unhandled errors.Condensing
into one line
wouldn't replace the old syntax, it would just be ergonomic.
Personally, coming from C, I don't mind the current syntax. As Robert said, it's one thing to write code, and another thing to read it. When reading Go code, its explicit, unsweetened syntax is generally a boon.
But I really don't see the downside here. You wouldn't even have to use the new syntax if you didn't want to.
gingerbill@reddit
I'm the creator of the Odin programming language and Odin does share a lot of similarities with Go; multiple return values being a big one. One thing I experimented with a few years ago was this
try
that you propose (since it was obvious from the get go). What I found is that:try
was a confusing name for what the semantics weretry
as a prefix was the wrong placeWhat I found is that just changing the keyword and its placement solved a bunch of problems. It was surprising to me it was purely a syntax problem. It became
or_return
.Most people were confused regarding the semantics of
try
, but as soon as I triedor_return
, it became obvious.I'd also argue that
or_return
is also not hiding any control flow now because it literally says what it does in the name.The article I wrote on it at the time: https://www.gingerbill.org/article/2021/09/06/value-propagation-experiment-part-2/
However, Odin has more than just
or_return
, it hasor_break
,or_continue
, andor_else
.or_return
on its own is not enough to be useful, and you do need the whole family. (And before people suggest I should have just added theor
keyword and then haveor return
etc, I did experiment with that and it was actually a very bad idea in practice for non-obvious reasons).But as I say in that article, my general hypothesis appears to be still correct regarding error handling. Go's problem was never the
if err != nil
aspect but thereturn err
.Go lacks a rich type system and the
error
system exacerbates this problem. This is why Odin doesn't have a degenerate type for errors where all errors collapse to it. In Odin, errors are just values, and not something special. I have found that having an error value type defined per package is absolutely fine (and ergonomic too), and minimizes, but cannot remove, the problem value propagation across library boundaries. Go does treat errors as values but theerror
interface is in practice a fancy boolean with a string payload and people don't handle the errors, they just pass them up the stack (which Griesemer states in the article).Go not having enums and tagged unions (and them being separate too rather than unified like ML-style unions, i.e. Rust) from the start is what Go needed but sadly doesn't have. I understand unions didn't exist because of their conflict with
interface
, but I do wonder if that's because they were trying to implement another kind of union rather than one that would work well with Go.P.S. I do agree with the decision to put all of this on hold for Go. Go's slower approach to implementing new constructs is a good thing in my opinion, even if it annoys people.
SIeeplessKnight@reddit
Wow thanks for the response! I didn't expect you of all people to respond to my random reddit comment.
I really like those error handling semantics. I was actually looking at Odin a while back, but became interested in Zig. I'll have to look more deeply at the language now.
At the moment I'm sticking to C and Go because every C replacement (e.g. Zig) is still pre-1.0 and frequent breaking changes are a dealbreaker for me. And I really don't like Rust.
To me the standout features of Zig were:
Explicit allocators. It's very clear exactly what is allocating memory, and where, because you have to pass the allocator to the function.
Optionals enforce null handling at compile time, and error unions enforce error handling
Defer makes cleanup simple and explicit
Granular compile time and runtime safety checks for different builds
Integrated, robust tooling: basically having gcc, make, valgrind, static analysis, unit testing (with perf), etc. all in one place.
Comptime
Incremental compilation and seamless cross compilation
No hidden control flow
Granular stdlib
No UB by default
Seamless C interop
I'd actually be using Zig right now if it weren't for the lack of proper async and the language instability. One thing about Go that is hard to give up is its concurrency model.
I actually think it could be translated properly to a language like Zig or Odin with a little creativity but it would have to be optional.
I've thought about it. It would be something like:
Lightweight, user-level threads (fibers) with an adaptive manual yield
Bidirectional typed channels (conduits) for thread safe communication between fibers, passed as send-only or receive-only to fibers
A lightweight work-stealing scheduler that coordinates and multiplexes fibers across OS threads (internal deque to hold fibers; each OS thread runs an event loop to handle the fibers the scheduler gives it).
You'd import and initialize the scheduler. Then you could add fibers to it and start it. But if you didn't want/need all this you could just use basic concurrency primitives.
I don't know enough about Odin to compare, for now I'm stuck with C and Go. It's not a bad place to be, but I know we can do better.
gingerbill@reddit
To compare Odin against what you have written
context.allocator
. Explicit allocators everywhere are a double-edged sword because most people never want nor need them, and they will be come lazy and just rely on global-ish allocators and then pass them around. I know this because even I was doing this when I was using custom allocators in my C code, well before Odin or Zig existed. Odin's implicitcontext
system exists precisely because I know most people will not use custom allocators (or many other things), and thus this allows you to intercept that code quite easily.Maybe(T)
is a thing in Odin. In fact it is just defined as the following:Maybe :: union($T: typeid) {T}
Maybe(^T)
being the same size as a pointerdefer
exists in Odincomptime
to solve.try
is hidden control flow. If you don't, then Zig and Odin don't any hidden control flow. If it is, then Zig and Odin have hidden control flow.foreign import
system, and I'd argue it is much much nicer than Zig's too. It allows you to define the linking stuff directly in the source code meaning you don't need a separate build system.As for you latter thing, "fibers" as you describe them are anything but lightweight to do in the way you describe. Sadly, to do what Go does, you need to have an extremely heavy runtime which is effectively sandboxed from third party code. This is why CGo is effectively a separate language. If a language is to have green-threads, which is what you are describing, you need to design the entire language and runtime around them from the ground up, and effectively have something akin to GC (or another form of automatic memory management).
SIeeplessKnight@reddit
Thanks for the detailed response! I appreciate it. I'm very interested in languages like Odin in general. I guess my only thing is I'm not sure when Odin will be stable. The versioning is a little unclear. But after 1.0 I'd be happy to try it out!
I think you can get around GC with green threads actually. You just need a contract system, but it does require a special kind of scheduler. That's something I'll get to one day when I write a library to demonstrate. I have too many other things on my plate at the moment.
gingerbill@reddit
Odin the language (not the core library) is effectively done. We have resorted to monthly releases so that we have a regular release schedule rather than sporadic releases which use the
x.y
approach.As for green-threads, you still need to design EVERYTHING around it. Memory management (e.g. GC) is the least of the concerns. As soon as you need to interact with the OS or third party code, you are kind of screwed.
SIeeplessKnight@reddit
I'll definitely take a look at the language.
And yes, I know you have to design a lot around green threads, but OS threads are very heavy and don't scale well for I/O bound tasks. It does make sense for languages like Odin and Zig to delegate that to external libraries though.
libdill (https://github.com/sustrik/libdill) is pretty close to what I'm talking about here. You don't necessarily have to have a heavy runtime, libdill is very lightweight and outperforms the traditional threading model for I/O bound tasks by orders of magnitude.
brutal_seizure@reddit (OP)
Tell me you don't understand interfaces without telling me you don't understand interfaces. lol
Errors can be any value which has a
String()
method. Therefore errors in Go can be any value and can contain any amount of information. Also, errors can be wrapped/unwrapped to provide context as they cross API boundaries.Ken Thompson, Rob Pike and Robert Griesemer got it all wrong hey??? .....Okay. 👍
gingerbill@reddit
I understand interfaces very well. You don't seem to understand what I am saying how
error
is treated in practice, and you've just demonstrated that you've not understood what I wrote.That "string payload" is the
String()
method. And the "fancy boolean" aspect is because most people rarely handle the error and just doerr != nil
. And because it can be ANYTHING, that's what I mean by the a "degenerate type".I know errors can be wrapped/unwrapped, even
switch
ed on too, but people rarely do anything with that because of how they use theerror
interface.brutal_seizure@reddit (OP)
In your opinion! I write Go for a living, in many big teams and you're wrong. Completely and utterly wrong.
Just because you've invented a procedural language that no one uses (hey so have I), doesn't mean you're right.
gingerbill@reddit
Thank you for the insults.
And I've written Go for a living too before. And yes, people do commonly not actually handle error states well in virtually any language, even Go.
Why are you making loads of assumptions about what my experience is when you could have just asked?
I am not going to continue talking to you because you are adamant on just insulting me for no real benefit.
brutal_seizure@reddit (OP)
Clown.
fromYYZtoSEA@reddit
The downsides are explained in the linked article. The one I immediately thought of was that it makes it impossible to wrap an error, so if you get an error “EOF” it’s hard to know where it’s coming from if it’s not wrapped like “request failed: error reading stream: EOF”.
The authors brought up some other interesting points.
Not necessarily defending the status quo, I’d love a more ergonomic solution too, but I can understand the downsides.
matthieum@reddit
There's no reason that
try
or?
couldn't build up a stack trace as they propagate the error.Heck, with Go being a GC'ed language, they could even automatically capture all variables in scope without any issue, and lazily pretty print them.
(I mean, no reason apart from performance, but Go isn't aiming for pure performance either, so clearly a fast enough solution would work)
SIeeplessKnight@reddit
The
try
syntax would just be sugar to propagate the unhandled error to the caller. If you wanted to handle and augment the error, you'd just doSo unless the argument is that err should never be propagated to the caller unaugmented, I think the
try
syntax (as described above) wouldn't detract from the language.fromYYZtoSEA@reddit
Yeah I understand that. My argument is that it is almost always helpful to augment an error (even though I wouldn’t go as far to say “never be propagated unaugmented”). The
?
syntax disincentivizes augmenting the error. For sure it’s optional, but if it doesn’t allow doing what many would consider best practice, then it’s not helpful.The article had another counter-arguments btw, including the ability to set breakpoints etc
SIeeplessKnight@reddit
Yeah that's fair enough. Certainly anything that wouldn't allow best practices would be a bad change, but if you wanted to add breakpoints you could just remove the sugar.
I think the argument for discouraging bad practices is the most compelling one.
ajr901@reddit
TL;DR: they considered several proposals, put out a couple of their own, but too many people chimed in and complained – even about the
?
borrowed from Rust that so many seem to laude as the solution. And so now they have chosen to do nothing about it instead of continuing to try to please everyone. They'll revisit at some point in the future.morglod@reddit
Zig's error handling is better
look@reddit
Go has a great feature set wrapped in a fundamentally flawed language. I suspect its popularity is temporary, and another, better language will eventually displace it (even if just another language on the same runtime).
I think it only even got to where it is now by chance through the Google Borg to k8s to DevOps vector.
If a FAANG had developed something like Crystal instead and used it in a transformative project like Borg, then I think we’d all be in a much happier place now.
zellyman@reddit
This is every language, though. Go has already had a pretty great run compared to most. I suspect a lot of that is that the people who like it like it because it's little more than a more plesant C that's dead simple to deploy. There's plenty of other amazingly feature rich languages out there to choose from, Go doesn't need to be something it isn't.
look@reddit
Agreed, but I think there’s still an opening at the app tier for a better solution. Go, Java, PHP, C#, Python, Ruby, Typescript, Rust, etc all have some deficiencies there in my opinion.
Main-Drag-4975@reddit
What are you missing exactly? Would something like typed Clojure be more your speed?
HighLevelAssembler@reddit
How so?
Asyncrosaurus@reddit
It doesn't do what I want it to do.
Go has been successful because it's so absurdly simple any idiot can pick it up and be productive in a weekend. All the Go haters want to tack on a bunch of complex features that would make the language no longer idiot proof, and they get really, really (unhealthily) upset that the designers don't want that.
cant-find-user-name@reddit
I work with go professionally. Go is definitely not idiot proof. There are so many footguns in go, there's books written on it. It is easy to pick up, but not easy to master and very trivial to make many many mistakes.
Asyncrosaurus@reddit
I'm exclusively using idiot proof to explain the ability to quickly and easily read and write the minimal language features. Go has resisted the perl-ification so many languages go through, adding syntactic sugar to do the same thing 6 different ways (e.g. modern C#).
Code quality hasn't much to do with being an idiot, some of the absolute worst bloated and incomprehensible code I've ever seen has come from some of the smartest developers I know. Usually the kind of abstraction and indirection that evolved out of the phrase "woudn't it be cool if...".
vips7L@reddit
I'm surprised one of them hasn't done this. Every thread about Go shows there is a huge desire for a natively compiled language that is more expressive. It just seems like none of the big players are trying to make it happen.
The closest I can think of right now might be Swift but it doesn't seem like Apple is going to invest enough to make it usable outside of its ecosystem. C#, Java, Kotlin are all stuck with a big language runtime and the other languages like D, Crystal, or Nim don't have a large backer to gain popularity.
chucker23n@reddit
I wonder.
They just relaunched swift.org, and if you go to Install, there are Linux and Windows tabs, with the Windows one even using the new
winget
package manager.Probably largely community-driven, but I wouldn't rule out that it's eventually of interest to Apple, too — they do have Windows companion apps, at least (e.g. https://apps.microsoft.com/detail/9np83lwlpz9k?hl=en-us&gl=US), and I imagine they would like if those could share more Swift code with their macOS/iOS counterparts.
ssrobbi@reddit
It’s very much of interest to Apple, they’ve invested a good amount into Linux support and server-side frameworks.
thehenkan@reddit
Apple is actually driving a lot of the support for other platforms, especially for Linux.
myringotomy@reddit
There are people who have done it. V was supposed to be a better go. That's the biggest one I can think of but I remember many posted on /r/ProgrammingLanguages like borgo, pipefish, etc.
vips7L@reddit
V is for Vaporware.
myringotomy@reddit
What makes you say that?
vips7L@reddit
Wild promises from the author that never materialized. The whole thing is akin to a rug pull.
myringotomy@reddit
I mean the project is still there and being worked on actively. What is missing?
l86rj@reddit
I had a look on V recently and I liked what I saw. It also just got on Tiobe's top 50, so maybe it's promising.
HomsarWasRight@reddit
I honestly love working with Swift. But I don’t do Apple development anymore, and the support for cross-platform work just isn’t there. Vapor is not bad for a web backend, but it’s not enough.
It really needs to be spun out.
AGCSanthos@reddit
Because it's a huge money sink.
Golang started in the pre-2010s Google which had a much different culture and ideas about funding. Compare that to nowadays where Google has killed several internal and external language and framework projects with layoffs along with it.
The big companies have all this sway and technical expertise but can't invest in great community projects anymore because they are beholden to corporate greed. Even Google 20% projects have become more focused on making money - with developers focusing more on things with easier metrics for their promo packets rather than actually cool things.
UltraPoci@reddit
Crystal looks interesting. Is it actually used, or is it like Nim, which is very interesting but nobody uses?
look@reddit
It is used in real, production software (e.g. https://lavinmq.com) but I don’t think it’s common. I just recently discovered it myself, but I’ve started using it for some things (nothing big yet, but in a business context).
myringotomy@reddit
Obviously some people use it :)
It's actually very pleasant to program in.
light24bulbs@reddit
Absolutely. Certain parts like the compilation and the concurrency/async are perfect. Other parts are just shit. Another language should come along, take the genius parts, leave the rest, and end up with something great.
Mysterious-Rent7233@reddit
What is this "fundamental flaw" and how is it more flawed than every other language?
myringotomy@reddit
The fundamental flaw with go is that the designers think programming languages should be a hindrance to creating abstractions. This goes counter to most languages where abstraction is the goal.
I could make a huge list of specific flaws but I am sure they have all been mentioned already by others. Some decisions they made just seem obstinate and purposefully cruel like lack of named parameters and default values in function parameters and the inability to specify defaults in structs, lack of union types etc.
All of these require developers to do backflips in order to accomplish mundane ordinary tasks other languages can handle with ease.
I should also add that it makes dealing with databases a HUGE pain in the ass. They even put an sql package with what kind of looks like result types in the sql package because people were just using pointers and I guess they didn't like that.
myringotomy@reddit
Crystal is a great language but with a tiny community and a tiny set of core devs who really seem to struggle.
The exact opposite of go.
Too bad though, I really liked using when I first tried it.
rusl1@reddit
I'm happy they didn't get that garbage feature from Rust
aguspiza@reddit
That is why everyone talks about Rust and nobody cares about Golang
brutal_seizure@reddit (OP)
Except you're completely wrong!
https://www.tiobe.com/tiobe-index/
Go: 7
Rust: 19
Rust is less popular than Perl, lol.
aguspiza@reddit
Tiobe means nothing about language innovation and the proof is that Java is still #4
zellyman@reddit
I mean, not to engage too deeply in a flame war, but Go's adoption eclipses Rust and it's not particularly close.
aguspiza@reddit
I never said anything about current adoption I said what is actually discussed. Nobody looks to go language looking for ideas or solutions, the innovation is happening in Rust.
rusl1@reddit
Yeah yeah sure. I work with rust and the bullshit of this language is intolerable. Good if you come from C/C++ but dog*hit of you come from other decent languages
usrlibshare@reddit
Another important part is that the vast majority of "solutions" provided, focused primarily on syntactic sugar for the default "passthrough" error handling, which seems like an important issue, until one becomes fluent enough in Go for the eye to simply skip
While it may have been nice to have that in grom the start, if such schemes were introduced now, they would go againstvone of the core design goals of go, that tgere should be one, and preferably only one obvious way how to do something.
BubblyMango@reddit
People not understanding Go downvoting someone saying the truth. Classic reddit
usrlibshare@reddit
That's fine, I take pleasure in the fact that all their downvotes won't change that Go error habdling will remain as it is 😎
NostraDavid@reddit
Solution:
Gauntlet:https://gauntletlang.gitbook.io/docs
It transpiles to Go and supposedly fixes several Go painpoints.
CpnStumpy@reddit
Honestly this is a rock solid idea - go makes a great IL, it lacks the expressiveness and type system necessary to build maintainable software at scale, but with a few improvements sugarred over it...
BubblyMango@reddit
While it does make for a great IL being minimal and all, you cant possibly say with a straight face that it lacks features for being used at scale if you take a brief look at reality.
thomas_m_k@reddit
Surprised they didn't switch to types going after the variable name, as seems to be the trend.
Dealiner@reddit
There was a thread about Gauntlet here the other day and the creator decided to change that in a future version after a few comments requesting it.
Personally I'm not a fan of this trend but I guess I'm in minority when it comes to potential users.
potzko2552@reddit
I think the rust trick is more about compiler support for the option type than the ? syntax specifically
For go the situation is different because people tend to use tuples rather than the option type
thomas_m_k@reddit
I think you mean the result type rather than the option type. The option type cannot carry error information like a tuple can.
Ok_Bathroom_4810@reddit
I never found the error handling to be that onerous. I like that it's explicit, and while it does add verbosity, any modern IDE can be configured to auto-complete it for you while writing or collapse it while reading.
The verbosity is actually less than try/catch if you error check every errorable function call. Of course most people don't check every function call when using a language that supports try/catch, but I'm not sure if that is really an advantage, as forcing you to consider the possible error conditions seems more like a feature than a bug.
nerdyintentions@reddit
This article gave me blue balls. I read with anticipation. It's finally happening. The day many thought would never happen has finally come. Just like generics. Reading every proposal that was shot down. Wondering what kind of crazy solution they came up with...
And then: sorry, we aren't going to do anything.
kitd@reddit
I'm probably in a minority, but I like handling errors as values precisely because of the above. Go's error handling encourages you to handle each potential error appropriately and allows you to add detailed context before returning the error. At 4am when you've been called out because prod's down, that is more valuable than stack trace IME. Doing the same with eg Java exceptions would require each call to be wrapped in a try/catch and, voila, you have just as much boilerplate.
I quite liked the Rust-ish
?
option mentioned in the article, but again you're missing the context part of that feature which is it's most valuable asset.Without any of that, error-as-values is perfectly workable. For me. YMMV ;)
thomas_m_k@reddit
I agree that context is important. In Rust, there is the popular
anyhow
crate which allows you to do that:Jealous_Aardvark1265@reddit
It's a shame that, for all the nice methods and syntactic sugar rust has for Result and Option, we have to resort to a crate to do the right thing (add context to an error when returning it).
PuzzleheadedPop567@reddit
Rust intentionally has a really small standard library, that is more concerned with building a common vocabulary and primitives to keep external crates compatible with each other.
Most higher level languages with batteries included standard libraries make a lot of assumptions that just aren’t acceptable for systems programming.
For instance, something even as simple has error handling is going to be different depending on if you are writing for an embedded device, a web server, or a CLI tool.
Anyhow isn’t acceptable for a lot of the use cases and environments that rust is designed for. It’s just a fact of life that most rust projects will have to pull in external crates.
sM92Bpb@reddit
I think it's because anyhow is not a zero-cost abstraction. Anyhow errors allocate to the heap because it's a dyanamic, non-fixed sized struct. The most optimal way would be to create your own error structs/enums and implement the Error trait but it gets tedious very quickly.
________-__-_______@reddit
It kind of is, but at the same time I don't see how the language itself could solve this. Attaching context to an existing error requires either generic soup (which ruins ergonomics), or heap allocations meaning it can't work on
#[no_std]
.Adding an anyhow-like catchall error type with context to the standard library would also likely result in more libraries returning that instead of "proper" error enums, which would be a shame.
kitd@reddit
Yeah, that's what I was thinking of, and is missing in the Go version.
thomas_m_k@reddit
Oh, sorry, I misread your comment a bit.
matthieum@reddit
I would note that Rust & Go have different values, leading to different trade-offs. In particular Rust holds up uncompromising performance as an important value.
In Go, there's no reason that an error could get a backtrace, or that
?
could add a frame to the error's backtrace incrementally, which while not "context" would give a lot more information than... nothing, the default in Rust.BenchEmbarrassed7316@reddit
This is not true. Reliability, safety and correctness are key features of Rust.
matthieum@reddit
So? Those are not mutually exclusive.
In fact, a lot of sweat has been poured, time and again, on ensure that the safe & correct API would also have zero overhead (after optimizations).
And whenever this cannot be achieved, the safe APIs are complemented by unsafe APIs so the final users can pick whether they want safe or fast.
BenchEmbarrassed7316@reddit
The average developer doesn't have that choice. He will get both a reliable and fast solution.
You simply don't understand the concept of
unsafe
.Rust safety is based on clear invariants such as "a pointer always points to existing data of the corresponding type". The compiler guarantees these invariants. When writing abstractions with unsafe, the compiler guarantees are transferred to the developer - he must make sure that the invariants are fulfilled.
But back to the topic of error handling - I meant that you are wrong in that Rust chooses speed of execution over correct error handling with added context. No, Rust allows you to add context to errors and does it much better than that.
You can check my explanation here:
https://www.reddit.com/r/golang/comments/1l2giiw/comment/mvwe4lb/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
sM92Bpb@reddit
I chuckled when someone said that it is error prone.
My experience is that it is too strict! That's why anyhow exists because you have to do so may map_errs and custom error types to satisfy the types.
BenchEmbarrassed7316@reddit
Are you wrong about Rust and the
?
operator.I wrote a detailed answer in the discussion of this post in a specialized subreddit:
https://www.reddit.com/r/golang/comments/1l2giiw/comment/mvwe4lb/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button
Keganator@reddit
Go absolutely needs something.
if err != nil {
return err
}
Is not a pattern, it's a pathogen. It's 3/4 of your code in any moderately complex go app. "Just learn to ignore it" is totally inexcusable.
BehindThyCamel@reddit
Perhaps it's my C background showing but I like the fact that there is no easy way out of it. In fact, if someone tells you to learn to ignore it, I think they simply don't get the design principles of Go.
Error handling should not be an afterthought in applications designed for resilience, which is what I understand Go was intended for. Much of the discussion I've seen about this issue was in the vein of "I don't want to be bothered with it". Well, I do. When I don't, I use Python or something. Haven't tried Rust yet.
Conscious-Ball8373@reddit
You shouldn't ignore error conditions and I like that Go forces you to think about them. I spend noticeably less time debugging weird stuff in Go than in other languages; there's a significantly higher chance that code which compiles is correct.
That's not the same thing as saying that it could use some syntax to make you think about error handling in a less verbose way though.
nullmove@reddit
Depends on perspective. There is absolutely easy way out of it, you can just .... not do any error checking at all. Simply do:
And no one is going to force you to handle error if you don't want to. But when you want to do the right thing, suddenly your code becomes fucking ugly, that's like punishing good behaviour.
Exactly which is why neither Go nor C is doing enough. In Rust, first of all the compiler would force you to handle things that can fail (Option/Result). And then it gives you syntactic sugar that makes handling errors way easier on the eye. That's strictly superior in both ways.
________-__-_______@reddit
I think you'd like the way Rust does it. It forces you to check whether or not an error occurred before you can access the value at all, you don't really have the option not to bother with it. At the same time ergonomics are much nicer because of some syntactic sugar and helper functions to transform error values.
tophatstuff@reddit
Yep. I like Go as a "better C". If that's what you want, it's great. If that's not what you want, you won't like it.
Jealous_Aardvark1265@reddit
I think this is more common, and trickier to add syntactic sugar for:
Keganator@reddit
Fair point. Or returning
nil, fmt.Errorf(...)
in a lot of cases too.And it's even more than just this, it's:
Where that Errorf format needs to be different depending on what was happening or what went wrong.
But then the call below it usually has has the same or similar boilerplate, and the call below it should too. It collects the errors as needed, building up the path. But, only if every piece of the code in the stack uses that pattern. If any piece partway through forgets to do it, then the information is lost. It's relying on convention that has evolved over time instead of a built in design.
Jealous_Aardvark1265@reddit
Yep.
So we just have to do the explicit thing. I think it's OK. I know lots of people hate it.
In the rust ecosystem they have a kind of alternative problem: there's syntactic sugar for returning errors, but adding context is fiddly (often), so it's normal to use a third party crate (anyhow) for it.
At least in go, people mostly just use what the language provided.
brutal_seizure@reddit (OP)
The Go community's stance is not "Just learn to ignore it".
The Go community's stance is "Deal with the errors, don't ignore them".
Plus this pattern is far and away simpler than anything other languages use, including rust.
HighLevelAssembler@reddit
Error handling should be 3/4 of your code in any moderately complex app. Robust production software needs to handle all kinds of errors gracefully. Errors are values and you can handle them (or not) however you'd like.
You shouldn't "just learn to ignore" error handling, it's part of the control flow.
myringotomy@reddit
Why though?
How come people are able to write complex programs in other languages where 3/4th of your code isn't error handling?
Also go doesn't force you to handle errors.
Keganator@reddit
I agree with you in principle. But...
if err != nil {
return err
}
or some variant of this is the majority of what's done, since errors can't really be handled in place in a lot of cases. Maybe some additional info can be added to it, such as wrapping the error in another error. At least that add some value to the code.
Agreed. That's why the stance from many in the community of "just learn to ignore it" is so awful.
Fancy_Doritos@reddit
You can already join two errors
Neurotrace@reddit
Such an embarrassingly managed language. It's consistently behind the times on basic, fundamental features and when given an opportunity to fix something that's widely hated they decide to do nothing rather than deal with some grumbling from a subset of people until they get used to it.
Just adopt something. ? from Rust is widely accepted as a reasonable solution. If you don't want to worry about it conflicting with ternaries or something, use a built-in macro like the try "function". Doing nothing is the worst possible choice
zellyman@reddit
I mean, this is kinda why I like it. It is what it is, and doesn't try to be more. And if I need more, I've got other options.
brutal_seizure@reddit (OP)
Hardly anybody uses rust so to say it's 'widely accepted' is just plain false. Perl is used more than rust.
Source: https://www.tiobe.com/tiobe-index/
Chroiche@reddit
But people who use rust actually like rust, ergo it's probably got some good, ergonomic ideas in there.
Neurotrace@reddit
TIOBE is a bad dataset https://nindalf.com/posts/stop-citing-tiobe/
l-const@reddit
let me remind you that the rust subreddit is bigger than Go's
larikang@reddit
What courage.
zellyman@reddit
I mean this, but unironically.
Keganator@reddit
Worse...
For the foreseeable future, the Go team will stop pursuing syntactic language changes for error handling. We will also close all open and incoming proposals that concern themselves primarily with the syntax of error handling, without further investigation.
They've decided to actively suppress discussion around the idea.
randomperson_a1@reddit
Literally 1984
sweating_teflon@reddit
Big Gopher?
Mysterious-Rent7233@reddit
They've decided that they do not want to provide (and moderate!) the forum for that discussion.
drakythe@reddit
Honestly I think that’s a good move, depending on how long they do it. This blog post has already sparked dozens of suggestions on reddit and hacker news about how to resolve the issue. I’m betting they saw that coming and given the seven years of attempts to handle this decided they didn’t want to be messing with all the suggestions the announcement would spawn.
matthieum@reddit
Arguably, it's a form of courage.
Let's face it, even if they picked an imperfect solution that many bemoan today isn't the ideal solution, in a few years time, most of the complainers would, pragmatically, adopt the solution regardless, and most complaints would die down.
Instead, by doing nothing, they stand to suffer the constant stream of complaints forever.
It's a courageous choice to make.
aboukirev@reddit
Now, that Go has generics, wouldn't it make sense to implement Result type and, perhaps, some syntactic sugar for it. That will not help the standard library - compatibility promise does not allow it. And any new code, including new methods and/or packages in the standard library could use Result.
argh523@reddit
Go doesn't have tagged unions to match on like Rust does (with enums). So a Result type in Go doesn't really improve things over a simple (value,error) tuple.
Also, Result in Rust is convenient because there is syntax sugar for it. If you don't have sugar, it's worse than what Go currently has (match every Result to unwrap; No ?, no let-else.. remember that?)
Also also, everything uses Result. If you don't have that, error handling is different depending on what library you use, or even which function you call. That is terrible for many reasons, including having to cast between different kinds of errors
"Adding" a Result type to a language later on doesn't really make sense. And the same is true for many other functional paradigms
aboukirev@reddit
I understand and agree in general but hear me out on a few things.
First, tagged unions are implementation details. So long as the type implements a specific interface, it should not matter. That is where syntactic sugar comes in. And I suggested that they add that sugar in my original message.
Not everything in Rust uses
Result
. When one implements bindings over C ABI, one has to wrap functions that return regular error codes with Rust functions that returnResult
.Perhaps, as a typical Rust community approach, they let developers come up with different implementations of
Result
-like types in the wild and then decide on recommended way of doing it.tanorbuf@reddit
They could make it so that the hypothetical
Result
type could be destructured into the classical/oldres, err
form (with one of them nil) to enable backward compatibility.They still would need some correspondence for
Into<Error>
though, I guess, to really match rust on this.gingerbill@reddit
I'm the creator of the Odin programming language and Odin does share a lot of similarities with Go; multiple return values being a big one. One thing I experimented with a few years ago
try
that you propose (since it was obvious from the get go). What I found is that:try
was a confusing name for what the semantics weretry
as a prefix was the wrong placeWhat I found is that just changing the keyword and its placement solved a bunch of problems. It was surprising to me it was purely a syntax problem. It became
or_return
.Most people were confused regarding the semantics of
try
, but as soon as I triedor_return
, it became obvious.n.b. I know
?
was also suggested but it's not exactly obvious when scanning code. And most of the time when people say the word "read code", they mean "scan code". And a single sigil like?
is really easy to miss.I'd also argue that
or_return
is also not hiding any control flow now because it literally says what it does in the name.The article I wrote on it at the time: https://www.gingerbill.org/article/2021/09/06/value-propagation-experiment-part-2/
However, Odin has more than just
or_return
, it hasor_break
,or_continue
, andor_else
.or_return
on its own is not enough to be useful, and you do need the whole family. (And before people suggest I should have just added theor
keyword and then haveor return
etc, I did experiment with that and it was actually a very bad idea in practice for non-obvious reasons).But as I say in that article, my general hypothesis appears to be still correct regarding error handling. Go's problem was never the
if err != nil
aspect but thereturn err
.Go lacks a rich type system and the
error
system exacerbates this problem. This is why Odin doesn't have a degenerate type for errors where all errors collapse to it. In Odin, errors are just values, and not something special. I have found that having an error value type defined per package is absolutely fine (and ergonomic too), and minimizes, but cannot remove, the problem value propagation across library boundaries. Go does treat errors as values but theerror
interface is in practice a fancy boolean with a string payload and people don't handle the errors, they just pass them up the stack (which Griesemer states in the article).Go not having enums and tagged unions (and them being separate too rather than unified like ML-style unions, i.e. Rust) from the start is what Go needed but sadly doesn't have. I understand unions didn't exist because of their conflict with
interface
, but I do wonder if that's because they were trying to implement another kind of union rather than one that would work well with Go.P.S. I do agree with the decision to put all of this on hold for Go. Go's slower approach to implementing new constructs is a good thing in my opinion, even if it annoys people.
BOSS_OF_THE_INTERNET@reddit
There are the languages that everyone complains about, and then there are the langiages no one uses.
chucker23n@reddit
I mainly write C#, but I find Swift's approach interesting:
throws
try
, but if you do so, your function, too, needsthrows
do
/catch
, which I bet is familiar to peopletry?
, which means: if the function fails, just assignnil
insteadtry!
— the runtime crashes if an error does occurIOW, this:
Can be shortened to this:
I love that, because "just make it
null
if the method errors out" is a very frequent scenario in C#.If .NET/C# were redesigned today, I would love if the Try-Parse pattern (returning a
bool
, with the real value in anout
param) were eschewed in favor oftry?
syntax.It's barely better than VB Classic's
On Error Resume Next
. Just… no thank you.myringotomy@reddit
Swift is really a well thought out language.
internetzdude@reddit
Personally, I'm happy if they do nothing because to me none of the proposals make sense. If there is an error you have to deal with it, and I'd rather have some boilerplate for returning it explicitly than some syntactic sugar for hidden control flow that really just passes the error to the next function anyway, let alone compound option types that do the same in an even more obscure way. I've been programming solely in exception-based languages before I came to Go, and they had no advantage. People just feel they have advantages when they ignore invalid states and errors and handle them too late, often in some kind of catch all clause that makes no sense.
igouy@reddit
submitted 2 days ago
BenchEmbarrassed7316@reddit
Error handling in Go makes no sense, here is my comment on this:
Errors can be expected (when you know something might go wrong) and unexpected (exceptions or panics).
Errors can contain useful information for the code (which it can use to correct the control flow), for the user (which will tell him what went wrong), and for the programmer (when this information is logged and later analyzed by the developer).
Errors in go are not good for analysis in code. Although you can use error Is/As - it will not be reliable, because the called side can remove some types or add new ones in the future.
Errors in go are not good for users because the underlying error occurs in a deep module that should not know about e.g. localization, or what it is used for at all.
Errors in go are good for logging... Or not? In fact, you have to manually describe your stack trace, but instead of a file/line/column you will get a manually described problem. And I'm not sure it's better.
So why is it better than exceptions? Well, errors in go are expected. But in my opinion, handling them doesn't provide significant benefits and "errors are values" is not entirely honest.
Java is the only "classic" language where they tried to make errors expected via checked exceptions. And for the most part, this attempt failed.
I really like Rust's error handling. Because the error type is in the function signature, errors are expected. With static typing, I can explicitly check for errors to control flow, which makes the error useful to the code, or turn a low-level error into a useful message for the user. Or just log it. Also, if in the future the error type changes or some variants are added or removed, the compiler will warn me.
aguspiza@reddit
you can not agree with everyone in everything.
Go with the fucking ? at the end.
happyscrappy@reddit
Article does eventually get to the issue that most errors aren't really handled anyway. Most cases more just propagate errors than actually handle them. But then the article goes on to act like adding stack traces is part of handling an error. I can't think of the last time that was true. Stack traces are used to flesh out bug reports really. They are used to log errors. It is in theory possible to use the trace as part of the handling, and handle one error different than another based upon the trace (execution state) when it happened. But I've never see it done, and I would suggest instead you would create a new type of error code when you recognize the particular failure, instead of trying to divine what certain configurations of stack traces indicate.
Most of these shortcuts given really are leaning into error logging or possibly propagation as the way that errors are dealt with. And sadly, I think they are often correct. That's most of the principle that exceptions are built on too, isn't it?
imscaredalot@reddit
I like the idea that LLMs are why it's not added and they are right.