iamcalledrob
a day 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/...
> 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.
logicprog
21 hours ago
Yeah, 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
21 hours ago
I 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
21 hours ago
I 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.
benrazdev
21 hours ago
My understanding is that using `error` as a return type performs type erasure and that the only information given to the caller is the value of the `e.Error()` function. I now understand that this isn't the case.
bccdee
21 hours ago
You'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
21 hours ago
Yeah, 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).
9rx
17 hours 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
12 hours 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.
user
8 hours ago
user
18 hours ago
nirui
7 hours 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
2 hours 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?
kllrnohj
20 hours ago
The 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.
iamsomewalrus
21 hours ago
I 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
21 hours ago
The 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
20 hours 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
19 hours ago
The 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.)
benrazdev
17 hours ago
>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.
it's only present when you downcast though?
tptacek
17 hours ago
It's not super ergonomic, but the information is there, in about the same density as it would be in a Rust app, supporting the same error handling strategies (ie, discriminating between transient and durable errors, &c).
lokar
18 hours ago
I feel that in general, in the past 20+ odd years there has been an over emphasis on complex control flow with errors.
Lots of different fine grained error types with complex logic spread out over several call layers.
ime it’s better to aim for simpler handling, which seems to match go
tptacek
21 hours ago
In 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.
wrenky
21 hours ago
They 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.
the_gipsy
20 hours ago
The 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.