14 ms·
This article makes a lot of great points about the shortcomings of Go. I don’t think explicit error handling is one of them however. I’ve previously spoken abou
by devjab 2y ago
This article makes a lot of great points about the shortcomings of Go. I don’t think explicit error handling is one of them however. I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up. From a technical standpoint that isn’t necessarily a good argument, but from a pragmatic standpoint and decades of experience… well I will take explicit error handling which happens exactly where the errors occur every day. You can argue that Rust does it in a more elegant way, and I prefer it for personal projects. For big projects with a lot of developers of various skill level joining and leaving I think Go’s philosophy is one of the sanest approaches to error handling in the modern world.
Staying in that lane. In my part of the world Go is seeing adoption that no other “new” language has exactly because of its simplicity. It’s not the best language, but it’s often the best general purpose language because it has a lot of build in opinions which protect you from yourself.
- XorNot 2y agoWhen I started trying to teach myself Rust, the error handling story fell apart on me very quick. Like as soon as I wanted to try and get sensible reporting in their, suddenly we were relieving libraries, adding shims and fighting mismatched types and every article was saying the same thing: haha yeah it's kind of a problem. I'm very, very unsold on explicit error handling compared to exceptions for practical programming. The number of things which can error in a program is far larger then those that can't.
- lkirkwood 2y agoI felt the same but after switching to anyhow and thiserror in pretty much every Rust project I work on I find it quite painless. It's not ideal to rely on crates for a core language feature but I never find myself fighting error types anymore. Have you tried those crates? Do you still hold that opinion?
- masklinn 2y agoYou don’t need crates for it, anyhow is basically a better Box<dyn Error>, if you just want the error signal you can use that. The main thing missing from the stdlib fur this use case is I don’t think there’s anything to easily wrap / contextualise errors built in.
- usrnm 2y agoThe problems you're describing don't exist in go. There is exactly one standard type that is used by everyone, at least in public API's, you can always just return the error to the caller, if you don't want to handle it in place. The main difference with exceptions in my practice is the fact that it's a lot easier to mess up, since it requires manual typing. This is probably my main problem with everything being as explicit as possible: it requires people to not make mistakes while performing boring manual tasks. What could possibly go wrong?
- XorNot 2y agoI think that sort of nails it: the problem with errors as values is errors become part of the type signature and put under user control, and the user can't really be trusted with that power. Even the simplest functions tend to have error states - i.e. floating point math can always wind up handing back NaN. So where I end up is, the default assumption is every function is effectively of a type MightError(T)...so why even make us type this? Why not just assume it, assume the unwrap handling code, and so you basically wind up back at try-catch exception handling as a syntactic sugar for that whole system.
- Yoric 2y agoThe drawback, on the other hand, is that all the Go code I've read (including the stdlib and all the dependencies of my current project) is using `fmt.Errorf` or `errors.New`, which means that you can't use `errors.As`, which means that you generally cannot handle errors at all.
- monksy 2y agoI would certainly argue against the claim that explicit error handling is far overkill. Where I agree: It forces you to think about all of the possibilities your code might generate. (This is more of a C question than it is with other languages) However, when abstracting blocks of code away, you don't always need to handle the error immediently or you may want to handle it down the stack. You're giving up a lot of readability in order for the language to be particular.
- eru 2y agoRust handles this much better. Error handling is still explicit, but it gives you the tools needed to make it less tedious.
- madeofpalk 2y ago> It forces you to think about all of the possibilities your code might generate. Except it doesn't actually. You can totally just ignore it and pretend errors don't exist. Lack of sum types/Result, and pointers as poor mans optional, really hinder's Go's error handling story.
- monksy 2y agoI agree with you that the sum types are much better and it's intentional that they wanted null support. (Which was a frustrating pick considering the modern dev languages out there). The coding style encourages the behavior you're talking about. We're back in the C days where you have a result and an error. It's not a good pattern.
- acdha 2y ago> It forces you to think about all of the possibilities your code might generate I’ve seen way too much Go code which never even tested the err value to believe that until something like errcheck is built in to the compiler. I do agree that this is a plus for the explicit model but that’s been a better argument for Rust in my experience since there’s better culture and tooling around actually checking errors. I’m sure there are plenty of teams doing a good job here, but it always felt like the one lesson from C they didn’t learn well enough, probably because a language created by experts working at a place with a strong culture of review doesn’t really account for the other 99% of developers.
- troupo 2y agoIt's actually very rare that it should be the caller who has to handle the errors. Go, however, forces you to spread your error handling over a thousand little pieces with zero overview or control of what's happening. Rust eventually realised this and introduced try! and ? to simplify this
- koito17 2y agoMore importantly, Rust has the notion of a result type and it is designed to be both generic and composable. A problem I often face in Go and TypeScript code is code that ignores errors, often unintentionally. For instance, many uses of JSON.parse in TypeScript do not check for the SyntaxError that may be thrown. In Go, it is common to see patterns like _ := foo.Bar() // assume Bar() returns error This pattern exists to tell the reader "I don't care if this method returns an error". It allows one to avoid returning an error, but it also stops the caller from ever being handle to the error. Also, the position of the error matters. While the convention in the stdlib is to return errors as the final value, this isn't necessarily followed by third party code. Similarly, errors are just an interface and there is no requirement to actually handle returned errors. Even if one wants to handle errors, it's quite awkward having to use errors.As or errors.Is to look into a (possibly wrapped) chain of errors. The benefit of Rust's Result<T, E> is that - position doesn't matter - there is strong, static type checking - the language provides operators like ? to effortlessly pass errors up the call stack, and - the language provides pattern matching, so it's easy to exhaustively handle errors in a Result The last two points are extremely important. It's what prevents boilerplate like if err != nil { return nil, err } and it's what allows one to write type-safe code rather than guess whether errors.As() or errors.Is() should be used to handle a returned error.
- DanielHB 2y agoI am pretty sure if it were for the Typescript creators they would not allow exceptions in the language, but they had to work within the confines of Javascript. Heck they even refused to make exceptions part of the type-system. It is unfortunate that many of Typescript developers still rely on throwing exceptions around (even in their own typescript code). Result types are totally doable in Typescript and you can always wrap native calls to return result types.
- abtinf 2y agoHaving programmed for over 30 years, including nearly a decade of C#, I would say exceptions are one of the worst ideas in all of programming. They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about. Even junior engineers have a trivial time debugging most go errors, while even experienced principles struggle with figuring out the true cause of a Java exception.
- rand_r 2y ago> never handled correctly I’ve seen this argument, but if you look at real golang code and examples, it’s just a bunch of “if err <> nill” copy pasta on every line. It’s true that handling errors is painstaking, but nothing about golang makes that problem easier. It ends up being a manual, poor-man’s stack-trace with no real advantage over an automatically generated one like in Python.
- swiftcoder 2y agoWhich could be solved in one swipe by adding a Result<T, Error> sum type, and a ? operator to the language. This is more a self-inflicted limitation of Go, then a general indictment of explicit error handling.
- rand_r 2y agoNothing prevents explicit error handling in Python either. Forcing explicit error handling just creates verbosity since no system can functionally prevent you from ignoring errors.
- kaoD 2y agoMany people against Go's error handling do not advocate for exceptions, but for a combination of an Either/Result type (for recoverable errors) and fully aborting (for unrecoverable errors).
- the_gipsy 2y agoThere are several shortcomings with go's error handling. The author heavily lies onto rust, so the alternative is not exceptions but a `Result<T, Error>` sum type. No stacktraces and error wrapping forces you to not only invent unique error messages. You must also conceive a unique wrapping message at every call-site so that you can grep the error message and approximate a stacktrace. The weird "return tuple" , which obviously just exists for errors because there is not a single other place where you can use tuples in the language, and the awkward variable initialization rules, make it so that you use the wrong `err` var at some point. E.g. if you want to reassign the result to an existing var, suddenly you have to declare `var err error`, and if `err` already exists then you have to reuse it. There should be an enum type in go, or instead of the bizarre "return tuple" mechanics exclusive for errors, they should have added a better syntax sugar for errors like rust's `?` sugar. Instead we have something extremely tedious and quite error prone. > it has a lot of build in opinions which protect you from yourself It does have opinions, but too often they seem to be there to protect the language from being criticized. Sadly, this works, as marketing (lying) is an important factor towards making a PL popular in today's market.
- KingOfCoders 2y agoExceptions are sum types, they just have different syntactic sugar.
- pema99 2y agoNot really. Exceptions usually imply unwinding the stack, and the ability to catch at any point throughout the callstack. Result types are just 'dead' data.
- zozbot234 2y agoThese are fully equivalent in outcome, though often not low-level implementation. You can use try...catch (called panic...recover in Go) to pack a normal and abnormal return case into the equivalent of a Result<> type. Or just pass an abnormal Result<> back to the caller to manually unwind a single "layer" of the call stack.
- pif 2y ago> I’ve previously spoken about my loathing of exception handling because it adds a “magic” layer to things which is way too easy to mess up. I kind of see your point. In this very moment, it doesn't matter whether I agree. What I don't understand, though, is why (typically) people who abhor exceptions are among the fiercest defenders of garbage collection, which does add a “magic” and uncontrollable layer to object destruction. Personally, having learned to love RAII with C++, I was shocked to discover that other languages discarded it initially and had to add it in later when they realized that their target developers are not as dummy as those choosing Golang.
- bsaul 2y agoHow does RAII works in concurrent systems ? It seems to me you need to add compile-time object lifetime evaluation (as in rust) which so far incurs a high toll on language complexity.
- maleldil 2y agoThe exact same way it works in Rust. C++'s RAII works the same as Rust's Drop trait. The object is released when it goes out of scope, and if it's shared (e.g. Arc), it's released when the last reference to it drops.
- Mawr 2y agoDifferent kind of magic. Needing to account for every single line of code being able to throw an exception is very mentally taxing, whereas the existence of a GC removes mental load of needing to account for every single allocation.
- stouset 2y agoWithout fail, every single person I’ve seen rave about go’s error handling compares it only to exceptions as if that’s the only alternative. On the flip side I have yet to find a person who’s familiar with sum types (e.g., Maybe, Option, Result) that finds the golang approach even remotely acceptable.
- DanielHB 2y agoYou hit the main gripe I have with Go, its types system is so basic. I get people raving type-correctness of Go when they come from Python but the type system in Go is simply pre-historic by modern day standards.
- masklinn 2y agoGo’s type system is not even impressive compared to python’s.
- orwin 2y agoDo you have a pydantic equivalent in go? Also modern typing in python is starting to be OK to be honest (well, if you consider typescript typing OK), so it isn't really a knock on Go :)
- Yoric 2y ago> Do you have a pydantic equivalent in go? I've been working on one [1]. But gosh, does go make it hard. [1] https://github.com/pasqal-io/godasse https://github.com/pasqal-io/godasse
- DanielHB 2y agoWell I was comparing to python codebases before they added type annotations
- Yoric 2y agoWhich, sadly, is still the case of too many dependencies. While I much prefer Python as a language, Go wins against Python by having a fresher ecosystem, with a higher baseline for type safety. Still pretty low with respect to Rust or mypy/pyright with highest settings, but much better than any of the Python frameworks I've had to deal with.
- Attummm 2y agoFor me, the issue with error handling is that while errors are explicitly stated, they are often poorly handled. Rarely have I seen the handling of multiple reasons for why an error might occur, along with tailored approaches to handle each case. This is something very common in older languages like Python or Java
- cnity 2y agoAs a regular Go user, I agree with this take. Though the tools exist, error wrapping and checking (with errors.Is and so on) is actually pretty rare in my experience. Positive example of good and appropriate usage here: https://github.com/coder/websocket/blob/master/internal/examples/chat/chat.go#L74 https://github.com/coder/websocket/blob/master/internal/exam...
- Cthulhu_ 2y agoThis is down to developer style and agreements though; Go has typed errors and a set of utilities to match them [0]. Not using those is a choice, just like how in Java you can just `catch (Exception e)` after calling a dozen methods that might each throw a different exception. [0] https://pkg.go.dev/errors https://pkg.go.dev/errors
- Yoric 2y agoInterestingly, every time (and I mean _every_ time) that I've tried to use `errors.As` on errors raised by lib code, I found out that the lib just went with "nah, I'm just going to use `errors.New` or `fmt.Errorf`", which makes the error impossible to match. So... I'd say that this is a fumble in the design of Go.
- cowl 2y agothis part of error handling is pure religion. it goes even against one of the most basic go tenents. that code should be easy to read not write. Try reading and understanding the logic of a particular method where 75% of the lines are error noise and only 25% are the ones you need to understand what the method does. yes it's noise because whenever read a codebase for the first time you are never interested on the the error edge case. first glance readability needs to tell you what you are trying to accomplish and only after what you are doing to make sure that is correct. on this point go's error handling is a massive fail. Notice that I'm not saying explicit error handling is bad. I'm saying the insistence that error handling needs to be implemented inline interleaved with the happy path is the problem. You can have explicit error handling in dedicated error handling sections
- Yoric 2y agoDo you have examples for the latter?
- cowl 2y agothe most basic example was the declined proposal https://github.com/golang/proposal/blob/master/design/32437-try-builtin.md https://github.com/golang/proposal/blob/master/design/32437-... Some people didn't like the "try" keyword it reminded them too much of exceptions, some people didn't like that they couldnt see a return inline (which was the purpose of the proposal in the first place). it's not that there are no solutions. the main problem is the go team's insistence to have "one true way" (tm) of doing something and unfortunately this gap between people who want to see every return inline and people who want to see the clean solution separate from the error handling is not something that can be bridged by technical means. the only solution is to implement both ways and lets see which one wins.
- Yoric 2y agoThis doesn't look meaningfully different from current error handling in Go. It's basically the same syntactic sugar as `try!` in Rust, isn't it?
- 2y ago
- flir 2y agoI see a lot of people say this about exceptions, and I don't have that problem. The exception bubbles up the stack until something catches it. Ok it's a different code path, but it's a very simple one (straight up). So you either catch the exception nearby and do something specific with it, or it bubbles up to a generic "I'm sorry there was a problem please try again later" handler. Honestly makes me wonder what I'm missing. Maybe it's because I don't deal with state much? Do the problems start to mount up when you get into writing transaction locks, rollbacks etc? But I don't see why you wouldn't have the same problems with Go's mechanism. Hoping to gain enlightenment here. [copied from a comment below]: They are just horrific gotos that any library can invoke against your code. They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about. Maybe this is it? I prefer a "fail early and often" approach, and I tend to validate my data before I perform operations on it. I don't want to attempt recovery, I want to spew log messages and quit.
- acdha 2y ago> They are pretty much never, ever handled correctly. And nearly always, after an exception is “handled”, the application is actually in an unknown state and cannot be reasoned about. I think that’s misdirected but illustrates the emotional reasons why people develop a negative impression of the concept. Usually it means someone had bad experiences with code written in a poor culture of error handling (e.g. certain Java frameworks) and generalized it to “exceptions are bad” rather than recognizing that error handling isn’t trivial and many programmers don’t take it seriously enough, regardless of the paradigm. As a simple example, C and PHP code have had many, many security and correctness issues caused by _not_ having errors interrupt program execution where the users would have been much better off had the program simply halted on the first unhandled error. If you write complex programs with lots of mutable shared state, yes, it’s hard to reason about error recovery but that’s misattributing the problem to the mechanism which surfaced the error rather than the fact that their program’s architecture makes it hard to rollback or recover.
- uzerfcwn 2y agoExceptions are difficult to discuss because different languages implement exceptions differently, each with their own downsides. That said, I don't think anyone has an issue with bubbling. Even sum type proponents love Rust's ? shorthand, because it makes it easier to propagate Results up the stack. The big issue with exceptions in C#, Python and JS is that they're not included in function signatures, which means you have to look up the list of possible exceptions from documentation or source code. This could be amended with checked exceptions like Java, but it allegedly doesn't mesh well with the type system (I haven't personally written Java to confirm this). And then there's the C++ crowd that slaps noexcept on everything for possible performance gains. Personally, I like the way Koka does exceptions with algebraic effects and type inference. It makes exceptions explicit in function signatures but I don't have to rewrite all the return types (like in Rust) because type inference takes care of all that. It also meshes beautifully with the type system, and the same effect system also generalizes to async, generators, forking and other stuff. Alas, Koka is but a research language, so I still write C# for a living.
- pansa2 2y ago> Go is seeing adoption that no other “new” language has exactly because of its simplicity Yes - for me, the simplicity is essential. As a part-time programmer, I don't have months to spend learning C++ or Rust. If my project needs to compile to small(-ish) standalone binaries for multiple platforms (ruling out Python, Ruby, Java, C#, etc) what simple alternative language is there? Plain C?
- martindevans 2y agoC# can compile standalone binaries for multiple platforms.
- acdha 2y agoBasic Rust doesn’t take months to learn, especially when you’re not trying to do things like distributing crates to other people. I found the compiler to be enough more helpful than Go’s to make them roughly time-equivalent for the subset of common features, especially for a simple CLI tool.
- damnever 2y agoGo's error handling is not explicit because error is an interface. So, a nil MyError is not the same as a nil error, and error comparison is inconvenient because the interface constraints are so loose that people often resort to checking errors with strings.Contains.