13 ms·
I cannot wait for the Result monad. The state of error handling in Go at the moment is embarrassing at best.
by linkdd 5y ago
I cannot wait for the Result monad.
The state of error handling in Go at the moment is embarrassing at best.
- papito 5y agoPeople downvote it, but it's true. 2/3 of your Go code is `if err` blocks. And the way you have to chain the error messages to make the stack make any kind of sense is just maddening.
- Vanclief 5y agoI used to really hate that. After reading this amazing post https://middlemost.com/failure-is-your-domain https://middlemost.com/failure-is-your-domain I created a module that makes this bearable https://github.com/Vanclief/ez https://github.com/Vanclief/ez
- neop1x 5y agoWe often have errors handled. In contrast to many Java codes just spitting unhandled exception stacktraces or crashing completely.
- papito 5y agoYou must handle an exception in a language like Java. Yes, you can just print the stacktrace, but that is a choice. I don't HAVE to handle errors in Go until the program crashes as well. I never really understood that argument.
- tptacek 5y agoA lot of people are going to write very un-idiomatic Go code with "Result" types built on simple generics that, 5 years from now, we'll all be kind of smirking at. Rust's Option and Result make sense because the language supports it (most importantly with match statements). They don't make sense here.
- linkdd 5y agoFun observation of mine: most articles/tutorials I've read about "idiomatic go code" are presenting ideas that you never see in any production code. Seems to me that the ones who write about "idiomatic go code" aren't the ones who are shipping softwares/libraries.
- nojito 5y agoThis is true of all developer evangelism in general.
- tptacek 5y agoThis seems pretty glib. There's pretty obviously a phenomenon where people try (or used to try) to write code for other languages (less commonly Rust, more commonly Ruby) in Go, and that code is kind of notoriously uncooperative with the rest of the ecosystem and hard to maintain. You'll find it in Go web stack libraries, in particular, or with code for people who tried to fake generics with codegen.
- preseinger 5y agoExample? The majority of Go I've been hired to improve has suffered immensely from not using accepted idioms. Most of my work has been focused on introducing those idioms systemically.
- the_duke 5y agoEmulating sum types in languages without pattern matching is extremely awkward, to the point of being almost useless. type Result[T] struct { ok: *T err: error } Great, so how do you work with it? * You can have a `ok()` getter that returns `*T`. Now you you need an `if x != nil` * `isOk()` + `isErr()` * `unwrap() T`, which panics on errors * `split() (*T, err)` that splits into separate values, especially awkward since you both need `if err != nil` AND dereference the pointer That API is more awkward then the status quo, and doesn't buy you any correctness guarantees because eventually you have to do an `if x := res.ok(); x != nil` or `x, err := res.split(); if err != nil {` anyway. Pretty much the only convenience you gain are functions like `map()` or `unwrap_or`, but eventually you always have to take out the value and without being able to pattern match you can't get an API that improves correctness much. (std::optional in C++ is a great example)
- kbd 5y ago> because eventually you have to do... Emphasis on eventually instead of at every single function call.
- linkdd 5y agoThe Rust Result type has: - map / map_err - and_then / or_else - unwrap / unwrap_or And countless of other functions making it very practical to chain computations without having to pattern match anything. I do the same in Erlang/Elixir. In Golang, I need to check every function call, and if I want to know where an error come from, I need to wrap it in an errors.New() because no exceptions = no stacktrace
- kbd 5y ago> and if I want to know where an error come from, I need to wrap it in an errors.New() because no exceptions = no stacktrace Zig manages to provide traces with very little overhead: https://ziglang.org/documentation/master/#Error-Return-Traces https://ziglang.org/documentation/master/#Error-Return-Trace... I am baffled as to why error handling in Go remains so impoverished.
- egeozcan 5y ago
- Thaxll 5y agoWell it's better than most language with exceptions. People makes it a big deal, in reality it's not.
- linkdd 5y agoAn exception has a stacktrace. This single piece of information is crucial when you debug and makes handling errors in Golang embarrassing. I rest my case.
- mseepgood 5y agoIf you want stacktraces just panic. But that's not proper error handling.
- divan 5y agoOr just print stacktrace with `debug.PrintStack()`
- pkaye 5y agoStack trace was part of a proposal to add to the error package but it didn't happen. You can use third party errors packages like https://github.com/pkg/errors https://github.com/pkg/errors which wrap errors with stack traces.
- randomdata 5y agoGo exceptions do carry stack trace information. However, the topic is errors, not exceptions. Those are very different concepts. Of what use is stack trace information in debugging values that you have assigned error meaning to when not other types of values? If you had a function func add(a, b int) int { return a * b } there would be no expectation of carrying a stack trace to debug it. So what's different about func add(a, b int) error { return errors.New("cannot add") } that does require a stack trace?
- dragonwriter 5y ago
- rp1 5y agoI’ve been doing a lot of rust programming recently, but how exactly is the Result monad better? I feel like I end up with nested match statements for chained results, but maybe I’m doing something wrong.
- linkdd 5y agoRely more on the map/map_err/or_else/... methods for the Result type. You'll get something like (pseudocode): may_fail_who_knows() .map(use_value) .map_err(some_error_processing) .and_then(another_computation_which_can_fail) .or_else(with_some_error_handling_that_can_rescue) .unwrap_or(a_default_value) Basically, instead of nested match expressions, you get a "pipeline".
- rp1 5y agoThat does clean stuff up a bit, but still requires nesting if you need to access more than one value generated in the chain at a time. Go's pattern of val, err := ... is a little cleaner in that regard, but does have a lot of redundant if err != nil checks.
- stouset 5y agoAnd let result = may_fail_who_knows()? .use_value_which_might_also_fail()? .use_a_different_way_that_might_fail()?;
- andrewjf 5y agoI would love to use this style but I find myself reverting back to the match syntax. match may_fail_who_knows() { Ok(success) => { do_something_with_success(success)? }, Err(failed) => { some_error_processing(failed) } } As `do_something_with_success` in a closure can't early return from the function (since it's in a closure), which makes sense, but just annoying to read nested results.
- wmanley 5y agoThis is equivalent to: let x = match may_fail_who_knows() { Ok(y) => Ok(another_computation_which_can_fail(use_value(x))), Err(e) => with_some_error_handling_that_can_rescue( some_error_processing(e)), }; match x { Ok(y) => y, _ => a_default_value, } It's a bit more verbose than using the combinators, but someone coming across it for the first time will understand it immediately because there's less to remember to understand it (this is where go really shines). Also: by avoiding functors there are fewer subtle lifetime issues and `move ||` stuff to deal with and you can return from the containing function and use the `?` operator. During the discussions of how `.await` was going to work for rust async there was the proposal to add other suffix keywords. So this would look like: may_fail_who_knows() .match { Ok(y) => Ok(another_computation_which_can_fail(use_value(x))), Err(e) => with_some_error_handling_that_can_rescue( some_error_processing(e)), } .match { Ok(y) => y, _ => a_default_value, } Maybe not that different.