Live data from Hacker News

Gopher Wrangling: Effective error handling in Go

stephenn.com

71–80 of 310 posts

Re: Gopher Wrangling: Effective error handling in Go

#71

Earlier quoted context omitted.

the idea that error handling "pollutes" code is a misunderstanding which go addresses the "sad path" of error handling is equally as important as the "happy path"

How does it address it? By making it painstakingly verbose (not to mention error prone) to deal with errors? Not referring to you personally, but I've heard that sentiment several times now, and I have not seen anything to back it up (as with several other golang claims).

given a function fn that can fail, it will return a result and an error e.g.

    result, error = fn(...)
calling this function should yield to the caller two possibilities, somehow: a success value _or_ a failure error

the important thing is that in both cases, the control flow is visible in the source code as written

    result, error = fn(...)
    if there was an error, ...
    if it was successful, ...
when an expression fails, you want to see the consequence in-line

the success path and the failure path are equally important

Re: Gopher Wrangling: Effective error handling in Go

#72
post #64

Earlier quoted context omitted.

Does the caller care? By day I work with a team in a language that sees errors ride on the exception handling system. Staying within the original example, I see code like this all the time ( too often , even, but that's another topic for another day): try { file = getFile() } catch(/* ... */) { fileUnavailable() } Here, the assumption of getFile that the caller wanted an error was incorrect. A Result-using language w…

But in Rust you can do the same, I do this all the time: let _ = fs::mkdir_all() // Error ignored, Rust will not complain because you explicitly assigned to _ Or if the function returns something I need but I don't care about the error: let Ok(file) = get_file() else { file_unavailable(); return } upload_file(file); Or this: let file = get_file().unwrap_or_default()

[flagged]

Re: Gopher Wrangling: Effective error handling in Go

#73

Earlier quoted context omitted.

please don't do this it obfuscates the control flow, specifically the value that is actually returned early returns on errors are good, not bad edit you want func foo() error { x, err := bar() if err != nil { return fmt.Errorf("bar: %w", err) } if err := baz(x); err != nil { return fmt.Errorf("baz: %w", err) } if err := bat(); err != nil { return fmt.Errorf("bat: %w", err) } return nil }

I’m all for generating that. I don’t want it in source where rereading it wastes expensive developers’ time and mistakes become possible.

nope

error handling (as expressed here) is equivalent in priority to core business logic

it absolutely belongs in source, because it is important for developers to see

Re: Gopher Wrangling: Effective error handling in Go

#74

Earlier quoted context omitted.

Doesn't the producer know best whether the producer failed?

Does the caller care? By day I work with a team in a language that sees errors ride on the exception handling system. Staying within the original example, I see code like this all the time ( too often , even, but that's another topic for another day): try { file = getFile() } catch(/* ... */) { fileUnavailable() } Here, the assumption of getFile that the caller wanted an error was incorrect. A Result-using language w…

You mean file == nil.

Re: Gopher Wrangling: Effective error handling in Go

#75
post #54

Earlier quoted context omitted.

In rust it is not possible to use incorrectly, and in go it is, sure. But whether it is possible or not is only one dimension. Does it matter that it’s possible to misuse errors in Go if it virtually never happens? I just don’t find the point about what is possible interesting. The other trade offs around readability, ergonomics, and so on seem more impactful.

> virtually never happens Ah yes, like it "never happened" in the Kubernetes project? - https://github.com/kubernetes/kubernetes/pull/60962 - https://github.com/kubernetes/kubernetes/pull/80700 - https://github.com/kubernetes/kubernetes/pull/27793 - https://github.com/kubernetes/kubernetes/pull/110879 I can find tons of these, just by searching any larger Go project's Github. Here's one from docker too: https://githu…

Kubernetes is a huge project and not idiomatically written, so it would be shocking if it didn’t exhibit everything that can go wrong with Go.

CockroachDB is a better example.

Re: Gopher Wrangling: Effective error handling in Go

#76
post #64

Earlier quoted context omitted.

But in Rust you can do the same, I do this all the time: let _ = fs::mkdir_all() // Error ignored, Rust will not complain because you explicitly assigned to _ Or if the function returns something I need but I don't care about the error: let Ok(file) = get_file() else { file_unavailable(); return } upload_file(file); Or this: let file = get_file().unwrap_or_default()

[flagged]

So what is the point of the distinction then? The examples I gave were idomatic Rust too.

Both Go and Rust hand the caller an error and the caller then must do something with the error. In both Go and Rust, you can assign the error to underscore and ignore it.

Java is different but we aren't talking about Java right now.

The only difference is that in Go the function can (and must) still provide a value for "file" even when there is an error. In Rust you can kind of do something similar, by giving the File struct a default value (that's what "unwrap_or_default()" is) but it isn't exactly the same. I would argue that such a case is extremely rare though.

Re: Gopher Wrangling: Effective error handling in Go

#77
post #32
post #15

Earlier quoted context omitted.

Rust's error handling and option types actually aren't monads, they just have similar ergonomics for end users. There's some tricks that have to be done to make them work in a eager evaluation context, and as a result implementing iterator combinators does not feel like working with modads.

In what sense aren't they monads? They have a bind method ("and_then") and a return method (Which is just the variant for constructing the success case, i.e. "Ok" or "Some"). It's more idiomatic to use "map", but that's just a degenerate case of bind. > There's some tricks that have to be done to make them work in a eager evaluation context... Monads have nothing to do with laziness, though. In Haskell, IO actions ar…

After doing some research to refresh my memory I found this old thread: https://users.rust-lang.org/t/what-is-a-monad-and-who-needs-...

I believe what I had originally told that makes them not monads is that because Rust goes through some convlutions to fake the laziness of Haskell monads, it makes them not be typed like Haskell monads.

For example, the declaration of the `.flat_map` on an iterator is actually `fn flat_map(self, f: F) -> FlatMap where Self: Sized, U: IntoIterator, F: FnMut(Self::Item) -> U`, which uses a "do-er" struct instance of `FlatMap` and is itself another iterator. Evaluating the entire monad-ish iterator combinator chain with something like `.collect()` or `.last()` or something is what triggers the evaluation.

Re: Gopher Wrangling: Effective error handling in Go

#78
post #49

Earlier quoted context omitted.

you don't want exception-style "convenience", that's the whole point you want to be able to read code and see a single control flow ? subverts that core requirement

The problem with exceptions is that they can come from any line of code and cause a "return". Rust's question mark solves the issue because it marks which lines of code can cause a "return". Therefore, you can always see see the control flow of a function. Moreover, you can go even further if you really really really want a single control flow. You can write a clippy lint to disallow early returns (ban "return" keywo…

? enables chaining, chaining subverts comprehensibility in exactly the ways i'm describing

Re: Gopher Wrangling: Effective error handling in Go

#79
post #69

Earlier quoted context omitted.

The Result approach believes that the producer knows what is best for the caller regardless of who the caller is. The Go approach believes that the producer shouldn't assume it knows the caller. I'm not sure one is better than the other, just different tradeoffs.

This doesn't really make much sense. The producer knows what it's returning. In the _vastly_ common case, it's either returning an error or a success object, but the Go type system is unable to represent that. The caller trying to pretend that the success object is there isn't a freedom the caller gets in the current system, it's an artifact of the type system not being powerful enough to encode the situation accurat…

> The producer knows what it's returning.

But doesn't know how the return values will be used by the caller. What is perhaps lost in this is where Go says that values should always be useful?

> If you have a function that can return both an object and an error, there still should be a way to represent that (exactly the current way).

Exactly the current way is what is said to be deficient, though. A function of this type is naturally going to return a file every time because a file is always useful, even when there is failure. Whereas Result assumes that you won't find the file useful when there is failure.

If you know the callers you can discuss if the file will ever be useful to the callers who use it under failure condition. Always useful does not mean always used. But Go, no doubt of a product of Google's organizational structure, believes that you cannot get to know your callers. You have to give them what you've got and let them decide what and what isn't important to their specific needs.

Tradeoffs, as always.

Re: Gopher Wrangling: Effective error handling in Go

#80
post #54

Earlier quoted context omitted.

> virtually never happens Ah yes, like it "never happened" in the Kubernetes project? - https://github.com/kubernetes/kubernetes/pull/60962 - https://github.com/kubernetes/kubernetes/pull/80700 - https://github.com/kubernetes/kubernetes/pull/27793 - https://github.com/kubernetes/kubernetes/pull/110879 I can find tons of these, just by searching any larger Go project's Github. Here's one from docker too: https://githu…

Kubernetes is a huge project and not idiomatically written, so it would be shocking if it didn’t exhibit everything that can go wrong with Go. CockroachDB is a better example.

This is a No True Scotsman fallacy. "No REAL Go code fails to handle errors."

OP demonstrated that failing to handle errors does in fact happen in the wild, while in Rust the compiler enforces that you must handle them. The question is whether this difference between the systems has practical implications, and it seems it does. The existence of community idioms that help avoid the problem doesn't change the fact that the languages themselves are meaningfully different.

Post reply on HN