9 ms·
Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug. I think the expectation that every single error is h
by jamespwilliams 5y ago
Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug. I think the expectation that every single error is handled and (importantly) wrapped explicitly is one of the reasons why Go and the Go ecosystem in general is conducive to writing robust and resilient systems. When writing Go, error handling is at the forefront of my mind, and not an afterthought, as it can be in other languages.
- nicoburns 5y agoHave you used Rust? Because it has the same explicit error handling, except it has syntax sugar (the '?' operator) which makes it painless. Additionally it's robustness is even better than Go's, as the Result type it uses for errors makes it impossible (compile time error) to access the return value without first checking for an error.
- toolslive 5y agoexactly! a little sugar would be most welcome.
- chrismorgan 5y agoFor comparison: fn my_func() -> Result<(), Error> { foo()?; bar()?; baz()?; Ok(()) } // --- let val = bar()?; println!("{}", val); // --- fn my_func() -> Result<i32, Error> { foo()?; bar()?; let val = baz()?; // ... more stuff Ok(val) } // --- fn my_func() -> Result<(i32, String), Error> { foo()?; bar()?; let (val1, val2) = baz()?; Ok((val1, val2)) // Those last two lines could even be just `baz()` or `Ok(baz()?)`. } Rather close to the world the author wants to live in.
- thiht 5y agoWhat happens if you want to log something before returning the error, or wrap the error, add some context or something ? Is there a way to override the ? operator? Or do you fallback to a matcher, which is roughly the same as Go error handling?
- maffydub 5y agoThe `Result` type has a `map_err` method to which you can pass a function (or closure) to modify the error, so you can write something like... ``` let (val1, val2) = baz() .map_err(|err| add_my_context(err, "some context"))?; ``` Also, the `?` operator automatically calls `into()` on the error that it was given, which will convert it into the desired error type (assuming there's a conversion implemented for it), so you only need the `map_err()` approach when you need to add context. I'm not sure what the idiomatic Rust way of logging before returning the error is - in the code I've written (and read), you'd add return the error with added context and leave the handler to decide whether to log. In my experience, it is pretty ergonomic.
- brundolf 5y agoYou can .map_err() before the ? to transform it inline using a closure
- ccakes 5y agoThe operator is just syntax sugar for something roughly this let foo = foo()?; // Expanded let foo = match foo() { Ok(v) => v, Err(e) => return Err(From::from(e)) }; If you're using your own error type then you can impl From/Into yourself to capture additional context from the source error (or a stacktrace) in addition to using map_err() as others have suggested
- nicoburns 5y agoIf you want to manually wrap/change the error somehow then the pattern would be something like: foo().map_err(|err| WrapperError::new(err))? For context, typically you would use a library like `anyhow` which allows you to do: foo().context("Context string here")? or foo().with_context(|| format!("dyanmically generated context {}", bar))? I don't know of any pre-built solutions for logging, but you could easily add a method to Result yourself that logged like: foo().log("log message")? or something like: foo().log(logger, 'info', "log message")? The code to do this would be something like: use log::error; trait LogError { fn log(self, message: impl Display) -> Self; } impl<T, E> LogError for Result<T, E> { fn log(self, message: impl Display) -> Self { error!("{}", message); self } } Then you'd just need to import the `LogError` error trait at the top of your file to make the `.log()` method available on errors.
- Cthulhu_ 5y agoI don't know Rust myself but, where / how are the errors actually handled? Or is this like unchecked exceptions?
- josephg 5y agoLike Go, the error is just returned from the function. The Result<> type is literally defined as “either an error value or a success value”. It’s up to the caller what to do with an error - panic, return it in turn, wrap it, or whatever else makes sense in context. It’s semantically quite similar to Go - except with better library support (Result instead of returning a tuple) and better syntax (the “?” operator instead of manual checks).
- chrismorgan 5y agoRust’s Result-based error handling is fairly similar to checked exceptions—all errors must be handled, whether you do something with them, ignore them or propagate them to the caller—but without the pain of checked exceptions, because the errors are just a normal part of the type system, which makes them much easier to work with in various ways (some of which are seen in the various comments here): the return type Result<T, E> is either a T (labelled “Ok”), or an E (labelled “Err”). The question mark operator just gives you an easy way of doing perhaps the most common handling of errors, which is propagating it up to the caller to worry about. So somewhere along the way you’re going to need to actually handle it (or explicitly ignore it), though it’s also possible in simple programs to let Rust handle it, by having your main function return a Result (that is, something like `fn main() -> Result<(), Error>`), in which case if it’s an Err it prints the error (roughly `eprintln!("Error: {:?}", err);`) and exits with status code 1.
- tialaramex 5y agoExceptions are a problem because they're a control flow change, whereas perhaps the error condition is just data. For truly exceptional situations, Exceptions are a good match because actually you wanted a control flow change. If the NetworkFailed maybe all of this complicated multi-node-synchronisation code is useless and we should handle that in code for NetworkFailedExceptions. But one program's exceptional situation is another's business as usual, our desktop network visualisation icon does not want to eat a NetworkFailedException, the fact that NetworkFailed is just a reason to change the little icon to red instead of green, not exceptional at all. This is particularly egregious when doing large data processing. If I doBackups() sending forty different sources to six backup servers, I actually don't want the failure of one backup server to handle one of the forty sources to blow up the entire function, I would much rather get back a detailed overview and go OK, 5 out of 6 ain't bad, no need to set off alarm bells and wake up the sysadmins. Error handling Go's way makes this a bit ugly but practical, Rust's way makes it feel reasonably ergonomic, in languages like C++ or Java it was kinda horrible (but C++ is slated to get an Expected class to mostly fix this in C++ 23).
- jamespwilliams 5y agoI haven’t used Rust. Does the ? operator add some kind of stack trace to the error? If not, how do you know where the error has come from? Having a Result type with statically enforced checking sounds good though.
- gilbetron 5y agoDoing foo()? is equivalent to: err := foo() if err != nil { return err }
- masklinn 5y ago> I haven’t used Rust. Does the ? operator add some kind of stack trace to the error? No. `?` performs an optional conversion and an early return, that’s it (kinda). > If not, how do you know where the error has come from? By default you don’t. There are utility libraries you can use to attach such information to error types (and eventually the stdlib will provide one), but OOTB an error value is… quite literally anything (it should probably implement `Error` but doesn’t actually have to). This is in keeping with the idea that hidden costs should be avoided when possible, and that reified results should be the norm so should have as little requirement as possible.
- kevinmgranger 5y ago> Does the ? operator add some kind of stack trace It depends. `?` will convert the inner error type (the value from the function to its left) to the outer error type (the return type of the function containing this line). That conversion function can be defined by the inner error type. If the conversion generates a stack trace (or adds to an existing one), then there will be one. Therefore, "Is there always a stack trace available?" is a tricky question-- it was seen as unimportant during the 1.0 days because they wanted to be able to accommodate low-resource environments. Today, there's third party error handling libraries that cut out error creation / manipulation boilerplate, that might create stack traces for you, and there's efforts ongoing to see how it might be added to the stdlib.
- sagichmal 5y agoThe ? operator permits chaining, which obscures sad path control flow and makes code harder to reason about, not easier.
- zozbot234 5y ago* Go language thread * top comment reply is about Rust Why am I not surprised at this. This style of trollish, overaggressive Rust evangelism is so obnoxious, it turns so many people off the language.
- piaste 5y agoNo, the response in this case was entirely pertinent: jamespwilliams: It is good for (Go's) error handling to be verbose, because it ensures that you handle every error instead of forgetting or ignoring them. nicoburns: It is possible to ensure every error is handled instead of forgotten or ignored without the need for verbosity, as demonstrated by Rust.
- zozbot234 5y ago> No, the response in this case was entirely pertinent Yeah, they always say that. How convenient that it constantly involves bringing up Rust Rust Rust when the thread is nominally about some other language. Just look at other HN top threads, it's a common pattern when PL's are discussed. And it's something that people will mistakenly blame on the Rust community.
- afranchuk 5y agoHave you considered that many PL hobbyists and/or enthusiasts recognize that many features of Rust (or other languages that may be brought up out of context, though in this case Rust is appropriate given the article content) were carefully, deliberately designed? It comes up often, I think, because Rust is one of only a handful of languages I can think of that is incorporating lessons learned, best practice, and innovations into every facet of its design and surrounding tooling. People who have researched, used, and (sometimes) suffered from a great variety of programming languages recognize the good that's being replicated and the bad that's being avoided. Aside from all of this, even if the article hadn't itself mentioned Rust, an article about a programming language's feature(s) is completely fair game to compare to other languages.
- simiones 5y agoThe fact that error handling is "at the forefront of your mind" is exactly the problem. Instead of following the code, you have to constantly deal with the irrelevant detail of error wrapping. 99.9% of code doesn't handle errors - it returns them further up the call stack (in rare cases with some extra context). This is exactly what exceptions do, without someone having to constantly write code for them to do so.
- deleted 5y ago[deleted]
- thiht 5y agoThe fact it’s explicit is a good thing though. Exceptions are a nightmare to handle and a worse mental overhead when dealing with code because you have to keep the possible exceptions in your mind.
- JulianMorrison 5y agoOr use an IDE that parses the language and helps you not notice that important parts of your program's behaviour are not visible from its text.
- simiones 5y agoBut that's just it: when writing code the right way, there are no important parts of the code's behavior that are not visible from it's text. For example, this code: void foo() { var res = acquireResource(); useResource(res); releaseResource(res); } is wrong even if no errors can be thrown. By contrast, this code is correct, regardless of whether any of these functions can throw: void foo() { try { var res = acquireResource(); useResource(res); } finally { releaseResource(res); } } Similarly, in Go this function is wrong: func foo() error { res, err := acquireResource() if err != nil { return fmt.Errorf("error acquiring resources while foo-ing: %w", err) } err = useResource(res) if err != nil { err = fmt.Errorf("error using resource while foo-ing: %w", err) } releaseErr := releaseResource(res) if releaseErr != nil { err = fmt.Errorf("%w; failed to releasing resource while foo-ing: %w", err, errW) } return err } even though I don't have any exceptions. The correct way to do this in Go would be: func foo() error { res, err := acquireResource() if err != nil { return fmt.Errorf("error acquiring resources while foo-ing: %w", err) } defer func() { err := releaseResource(res) if releaseErr != nil { logInSomeWay(fmt.Errorf("failed to releasing resource while foo-ing: %w", err)) } }() err = useResource(res) if err != nil { return fmt.Errorf("error using resource while foo-ing: %w", err) } } So overall, whether a function finishes early SHOULD be irrelevant.
- jillesvangurp 5y agoI prefer Kotlin on this front. They are interoperable with Java but they downgraded Java's checked exceptions to non checked exceptions. So, they don't force you to litter your code with try .. catch or throws statements. Additionally, with recent releases they use value classes for representing outcomes where failure is an expected outcome (i.e. not exceptional). That's similar to what Scala and other functional languages are doing. That and de-structuring makes dealing with errors a lot less tedious. It's actually not that dissimilar from what go does but just a bit nicer to deal with. The whole attitude with Kotlin is that exceptions are exceptional and that you probably won't be doing anything productive with them beyond logging. Using exceptions for conditional logic is a bit of a design smell. Sometimes you have to with some older Java libraries. But it's not what you are supposed to do. Otherwise, catch exceptions centrally. When using co-routines, uncaught exceptions actually cancel the co-routine scope (and the the other co-routines in them, if any), which gives you an opportunity to deal with that in a sane way. You can have error handlers attached to those to deal with that. Structured concurrency is a buzzword they like to use for this notion. I've been on a few Go projects; never as a main developer but enough to understand what is happening. I appreciate its simplicity; it's easy to get into. That is IMHO the main value of go: getting stuff done. But the verbosity is a bit painful if you come from something higher level. With Kotlin's native compiler slowly getting usable, I could see myself opting for that where I might have used Go or something else in the past (e.g. command line tools).
- jlouis 5y agoI agree with this point. Errors is a natural important part of programming. You can try to make error handling easier through sugar, but in the end you need to handle the error one way or the other. A proper sum-type would help in Go, but that's at best a more convenient and better way of handling the error. The error doesn't magically disappear.
- zarzavat 5y agoThe older I get, the cleaner I want my code to be. Reading code is the best way to prevent bugs. If the code is strewn with error handling ceremony then it's hard to get to the essence of what the code is doing, and it makes it more likely that bugs will go unnoticed.
- vlunkr 5y agoPersonally I think Go code is incredibly clean, even with the error handling. It's just little if err != Nil statements that you essentially speed-read. And is knowing what errors are possible in your code not an important part of reading it? Why should that be obscured?
- iainmerrick 5y agoThat’s why checked exceptions are good!
- JyB 5y agoThat's funny when Go's main focus is readability at the cost of writability. That's one of its main tenets... Some HN comments toward Go are a bit baffling.
- azth 5y agoI find golang much harder to read due to its extreme verbosity that detracts away from the logic the code is trying to do.
- iainmerrick 5y agoI just picked a random-ish file from the standard library to see what the error handling really looks like in practice (I went for "io" because I figured that's where errors will always crop up): https://cs.opensource.google/go/go/+/master:src/io/fs/glob.go https://cs.opensource.google/go/go/+/master:src/io/fs/glob.g... Does that seem reasonably representative? To a non-Go programmer, it doesn't seem that bad, but it sure does have a lot of error-handling boilerplate -- squinting at it, something around 50% of the non-comment lines? It just seems kind of weird, if you have to write "err != nil" and "return nil, err" over and over, not to have any syntactic sugar for it! There's what looks like missing return values on lines 64 and 69, then down on line 93 there's an explicit "return // ignore I/O error". Is this file outdated, or otherwise not a good example? I used Google to find it, and it says "Copyright 2020" in the header.
- llimllib 5y agoit's not outdated, and it's a fine example - reading the go stdlib is the best way to learn the language well, honestly. The missing return values on 64 and 69 are the "Named Return Values" thing that OP mentions - the returns are the variables named in the function declaration.
- iainmerrick 5y agoAha, that explains it, thanks!
- ryandvm 5y ago> Maybe it’s just Stockholm Syndrome, but IMO Go’s “verbose” error handling approach is a feature, not a bug. No offense, but I think it's Stockholm Syndrome. It reminds me of what the JavaScript folks used to say 5 years ago: "No, it's not a terrible language. I love JavaScript's callback hell." Of course once it gained promises/await, now they _really_ love it. I've been using Go for the last year and while I do genuinely enjoy the simplicity of the language, Go does have these annoying little customs like pebbles in your shoe. You can _say_ it forces you to think about error handling, but I find it does not. To be honest, I'm able to not think about error handling just fine. It just forces me to write boilerplate when I'm trying to crank out a quick non-robust prototype.
- kardianos 5y agoI find people who complain about Go's error handling don't understand how to program with Data. They want to type everything out. I say make a list of data, then loop through it once. Ruby people often seem to think, I know, I'll just write the code each date item at a time. Especially in unit tests. Wow. So if you think Data first, Go is great. If you think in terms of method chaining and verbs or nouns first, then Go won't treat you kindly.
- janslow 5y agoAgreed. I wasn't originally a fan of Go's error handling, but I do find it forces me to explicitly handle the error, I very rarely use a plain `return err` statement, instead wrapping it with additional parameters. Whereas when I use Rust's Result types, I tend to not add any additional context with `map_err` (until I need to investigate the cause of an error and have to retrofit it). There's still definite room for improvement though (I think the Go 2 proposal will help a lot).
- Zababa 5y ago> I think the expectation that every single error is handled and (importantly) wrapped explicitly is one of the reasons why Go and the Go ecosystem in general is conducive to writing robust and resilient systems. Sum types and pattern matching to enforce that would be even bettern. What I don't like about Go error handling isn't that it's "verbose" but that it's primitive.