4 ms·
I'd encourage the author to spend more time learning Go. They've come to incorrect conclusions -- especially regarding errors. Read more of the stdlib to see ho
by iamcalledrob 9mo ago
I'd encourage the author to spend more time learning Go. They've come to incorrect conclusions -- especially regarding errors. Read more of the stdlib to see how powerful they can be, e.g. net.OpError: https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/net/net.go;l=472 https://cs.opensource.google/go/go/+/refs/tags/go1.25.5:src/...
> The user now has an interface value error that the only thing
> they can do is access the string representation of ... The only
> resort the consumer of this library has is to parse the string
> value of this error for useful information.
This shows a lack of understanding about the `error` interface, `errors.Is`, `errors.As`, error wrapping etc.
Personally, I think Go errors are fantastic, just the right sprinkling of structure on top of values. And panic/recover for truly exceptional circumstances.
- bccdee 9mo agoYou're right, but I still think they have a point. What I miss from `error.Is/As` is exhaustive matching. I'd love a way to statically guarantee I haven't missed an important error type. It really comes back to the absence of sum types.
- packetlost 9mo agoYeah, the lack of sum types of any kind in Go is the only thing that I really miss when coming back from Rust. It's, I think, a big part of the reason that Gleam has seen a lot of growth. It has many of the syntactic benefits of Go with the compiler guarantees of Rust (though it's weird in some other ways, like dramatically favoring continuation-passing-style in programmer facing syntax).
- deleted 9mo ago[deleted]
- 9rx 9mo ago> I'd love a way to statically guarantee I haven't missed an important error type. It wouldn't be hard — rather easy, even — to write a static analyzer for that if you constrained your expectations to cases where sum types could practically be used. No reason to not do it right now! But sum types don't really solve for a lot of cases. Even in Go you can add additional constraints to errors to get something approaching sum types. You don't have to use a naked `error`. But you are bound to start feeling pain down the road if you try. There is good reason why even the languages that support defined error sets have trended back to using open-ended constructs. It does sound great in theory, no doubt, but reality is not so kind.
- bccdee 9mo ago> There is good reason why even the languages that support defined error sets have trended back to using open-ended constructs. Rust does it that way & has never trended in any other direction. The generally-accepted wisdom is "thiserror¹ for libraries, anyhow² for applications"—i.e., any error that's liable to be handled by a machine should be an enum of exhaustively-matchable error kinds, whereas anything that's just being propagated to the user should be stringified & decorated with context as it bubbles up. Certainly, unified error types which wrap and erase more specific errors are sometimes desirable, but equally they often are not. Languages which support exhaustive matching support both, permitting us to choose based on context. [1]: https://docs.rs/thiserror/latest/thiserror/ https://docs.rs/thiserror/latest/thiserror/ [2]: https://docs.rs/anyhow/latest/anyhow/ https://docs.rs/anyhow/latest/anyhow/
- deleted 9mo ago[deleted]
- Ferret7446 9mo agoExhaustive matching is terrible for backward/forward compatibility.
- bccdee 9mo agoYeah, your error handling should not always be backwards/forwards compatible. If the new version of a library is throwing a new kind of error, that library's clients need to be updated. Being able to break your clients when necessary is a feature & not a bug. Of course, if you don't need to break your clients, simply don't change your ErrorKind enum. Backwards/forwards compatibility matters a lot across service boundaries. I'm not saying Protobuf needs to be exhaustively matchable. But native types from a static library? You want to be able to match those exhaustively.
- logicprog 9mo agoYeah, I'm brand new to learning Go, but that was the first thing I thought when I got to his section on errors. You're supposed to return a generalized error and then use errors.is or errors.as to figure out what specific type of error it is so that you can access the specific data on it. And the reason you do it that way instead of just returning a concrete error type is because one function might want to return different error types depending on what specific thing happened. Just like you can throw different exception types in other languages. And obviously if that's happening, the reason you can't just directly access stuff without doing errors.is or as is because you don't know which it is until you do that. I think it's a shame that Go doesn't have sum types, exhaustiveness checking, and pattern matching, because it would make its error model more enforceable and concise (see Gleam!) but given that it doesn't for whatever reason, I think the solution it's gone with is genuinely the best possible second option that gives you 90% of the benefits of result and option (out of band errors, structured error data, errors as values so you can directly and in line decide what to do with them without special control flow, no invisible or non-local control flow, it's immediately obvious when anything can return an error, etc). And I say this as someone who started out hating go.
- ljm 9mo agoI think Go's concept of error wrapping is probably unusual to newcomers to the language who might be used to, say, pulling in a dependency for error handling (logrus or whatever) when it's all there in the stdlib in what Go has decided to be the idiomatic way to do errors and logging. It's nice when you understand how to do it well and move on from, say, printing errors directly where they happen rather than creating, essentially, a stack of wrapped errors that gets dumped at an interface boundary, giving you much more context.
- dematz 9mo agoI think it's probably confusing until you understand interfaces, which coming from other languages you might not be familiar with (my guess for what happened in this blog post). If you don't know what an interface is then maybe you assume err.Error() is some kind of string, without realizing Error() is just a required function but the err could be whatever type.
- iamsomewalrus 9mo agoI use Go as my preferred language and I think the author is mostly right. There’s no way for me to know or even check what are the possible errors this function can return? Sure, sometimes a comment in the library might be illuminating, but sometimes not. I agree that errors as values that I can handle at the call site rarely feel useful. Some of the Is, As ergonomics have improved, but damn if coding agents don’t love to just fmt.error any code it writes. Thus hiding the useful information about the error in a string.
- tptacek 9mo agoThe article claims: "The user now has an interface value error that the only thing they can do is access the string representation of." This is false. Didn't used to be, but that was many years ago.
- benrazdev 9mo ago>this is false. It's mostly false. Technically, if you use `fmt.Errorf`, then the caller can't get anything useful out of your errors. Types are promises. All the `error` interface promises is a string representation. I acknowledge that most of the time there is more information available. However, from the function signature _alone_, you wouldn't know. I understand that this is more of a theoretical problem then a practical problem but it still holds (in my opinion).
- tptacek 9mo agoThe subtlety missed in these conversations is that almost all the information there is for typical error handling --- that is, all the information that would be present in a typical Rust error handling scenario as well --- is encoded in the type tree of the errors. (Rust then improves drastically on the situation with pattern matching, which would simply improve Go with no tradeoffs I can really discern, just so we're clear that I'm not saying Go's error story is at parity with Go. But I'd also point out that Rust error handling at a type level is kind of a painful mess as well.)
- wrenky 9mo agoThey are an improvement over most languages, but the are _not_ better than sum types for the reasons listed above. Way less flexible and forces a lot more verbosity when compared to more functional languages. Hes definitely wrong on how the error types can help account for that but it would be best in class if we could properly chain them AND use all the greatness from interfaces.
- tptacek 9mo agoIn fairness, the idea that you needed to scrape error strings to handle errors was a very popular complaint about Go that was valid 10 years ago, including in the standard library.
- the_gipsy 9mo agoThe author is wrong, there exist mechanisms to cast errors. But they all suck! It's all just guessing and grepping for -hopefully- unique error messages.
- kllrnohj 9mo agoThe author is clearly aware of `error.Is` as they use it in the snippet they complain about. The problem is Go's errors are not exhaustive, the equivalent to ENOTDIR does not exist. So you can't `errors.Is` it. And while Stat does tell you what specific error type it'll be in the documentation, that error type also doesn't have the error code. Just more strings! Is this a problem with Go the language or Go the standard library or Go the community as a whole? Hard to say. But if the standard library uses errors badly, it does provide rather compelling evidence that the language design around it wasn't that great.
- yencabulator 9mo ago> the equivalent to ENOTDIR does not exist https://pkg.go.dev/syscall#ENOTDIR https://pkg.go.dev/syscall#ENOTDIR
- nirui 9mo ago> e.g. net.OpError I have my own complain against Rust's error handlings, but I can't bring myself to praise what Go has been doing. One problem with the error interface is that you can't know exactly which error type will be returned, and the Go authors may add new error types form time to time. `net.OpError` itself is a great example for that, which is a newer type than net.Error. This style of error signalling is best used when the error handling is binary, either "No error, continue" or "Errored out, abort", but not branched out path like "If encountered error A, do this. If encountered error B, do that. Otherwise, abort". Rust's error handling encourages such branched out handlings by default, but if you want to so the same in Go, there will be a nightmarish manual type discovery and tracking operation waiting for you. So for me, I rather use a dedicated signal code to tell which error branch I should do next rather than relying on the error, i.e: if next, err := func(); err != nil { // If errored, abort it return err } else if next == condition1 { return op1() } else if next == condition2 { return op2() } else { panic("unsupported condition") }
- iamcalledrob 9mo ago> This style of error signalling is best used when the error handling is binary, > either "No error, continue" or "Errored out, abort", but not branched > out path like "If encountered error A, do this. If encountered error B, > do that. Otherwise, abort". If I'm understanding you correctly, you can implement your example in a fully idiomatic way, and the stdlib does this in a bunch of places. Errors are just values, so you can return whatever best suits the use-case. The gamut of expected errors might not be part of type signature, but it can be part of your documentation. The "signal code" you refer to could be implemented via: 1. Returning error structs (e.g. ErrFoo, ErrBar) and using `errors.As()` access those structs -- useful if you have a highly structured error with additional fields. type ErrFoo struct { // fields // } type ErrBar struct { // fields // } if var errFoo *ErrFoo; errors.As(err, &errFoo) { // handle ErrFoo } if var errBar *ErrBar; errors.As(err, &errBar) { // handle ErrBar } 2. Returning a single error struct (MyErr) with an `Op` field, as `net.OpError` does. Similarly accessing via `errors.As()` // top-level, e.g. exported by package type MyErrorStruct struct { Op string Path string } var ( OpFoo string = "foo" OpBar string = "bar" ) var opErr *MyErrorStruct if errors.As(err, &opErr) { if opErr.Op = OpFoo { // handle OpFoo at path opErr.Path } if opErr.Op = OpBar { // handle OpBar at path opErr.Path } } 3. Returning predefined errors, e.g. `var ErrFoo = errors.New("foo")` and checking for them via `errors.Is()` // top-level, e.g. exported by package var ErrFoo = errors.New("foo") var ErrBar = errors.New("bar") if errors.Is(err, ErrFoo) { // handle ErrFoo } if errors.Is(err, ErrBar) { // handle ErrBar } Sidenote: I feel like people struggle to internalise that Go errors are just values* and are therefore are wildly flexible, yet not special in any way. I wonder if this is because other languages treat errors as something special?