As a gopher who has written an errors package for Go, I find Rust's journey into figuring out errors to be very interesting. A lot of the stuff in this article is similar to many of the error patterns in Go. Of course, with Go, it's a lot more ad-hoc and less boilerplate, but also obviously fewer compile-time guarantees.
The try! macro is something I know a lot of people would love to have in Go (those pesky if err != nil { return err } lines really bother people for some reason).
I think it's interesting that the built-in error type has a cause, that seems to be one of the most common patterns in Go error packages as well. Is there any automatic stack tracing? That seems to be one other common thing in Go error packages.
Yeah, it's very evocative. It also properly conveys that it's for extreme states, not usual error handling.
The motivation was actually to tease apart the two meanings of 'fail': fail! or Result? Now, fail means Result, and panic means panic! (previously fail!).
Most of the patch was actually updating _all_ of the mentions of fail in the documentation, to switch some of them to panic.
Sometimes if you want an error to have a specific type, but have a custom error message with context in it (like the name of the file that was missing, for ErrFileNotFound) you can do something like this:
type ErrBar struct {
error
}
Then you can return errors of that type thusly:
func Do(s string) error {
// ...
return ErrBar{ fmt.Errorf("failed to use %q", s) }
}
Finally, a lot of the time you just want to give the caller some information about an error you received from a function you called, so you just wrap it thusly:
That'll give you an error with a useful message (adding context to the error from getConfig), but with an anonymous type.
And since errors are just regular values, you can check for them in all the same ways you would for any other value (== for errors that are static values, type assertions to check for the type, etc).
It's not super different from Rust's errors, except that with embedding and the implicit interfaces, you don't have to explicitly tell the compiler quite so much to make your error a valid error.
Rust's main difference is the ability to define some automatic conversions and the try! macro that will do an automatic return if the call fails... which are definitely cool.
That's just a matter of having a built-in struct for generic errors no? Rust could probably provide a trivial error type (implementing the relevant traits) for this case.
Yeah,that's a good point. Certainly Rust or anyone else could implement a simple function that returns a populated error struct with the given string message.
I guess my initial reaction of a lot of boilerplate was mainly due to the default Rust error having a message, description, and a cause, instead of Go's default error that just has the message. Adding a description and cause is easy enough, and if you defined them for every error in Go, it would probably look pretty similar. My apologies.
> I guess my initial reaction of a lot of boilerplate was mainly due to the default Rust error having a message, description, and a cause, instead of Go's default error that just has the message.
The `Error` trait in Rust only demands that you define the `description` method.[1] The other two are optional.
Which is not something you generally do a lot of. Most functions either pass an error through or simply return a string error through errors.New(). (That's often not a cheat, either; my modules tend to declare the set of errors as globally-exported variables, allowing you to use == to figure out which error you're getting if you care. If your errors don't take parameters that works fine.) And the error interface only has one method instead of three. Less boilerplate in the worst and the common cases, less compile time guarantees.
It is debatable whether passing errors through is a good idea in theory or in practice at scale (two separate questions there). So far it hasn't bit me at all, though, and I've got a good idea of what I'm looking for for such "bitings".
I find in practice there's two types of errors: Errors I know how to do something about, and errors I don't. In the first case, I can only be dealing with a certain restricted set of errors coming back and I must inevitably write special-purpose code for the given error, and in the second, I don't much care about what it is so I often send it along untouched. I haven't found anything that manages to split the difference yet. Even returning a wrapped error is rare in my application code, though it has its place in library code.
I'm also finding in my real code that the ratio turns out to be closer to 1:2 errors I can do something with/errors I can't, which makes the explicit handling not matter so much. (My gut feeling is that if you find you are always just "return err"ering, and especially if you are doing it without thought, you're writing code that is passing up on some robustness.)
Yeah, I agree about wrapping. I actually usually try to avoid just doing return err, because you're losing some context that this code understands that the below code doesn't.... like if you return a FileNotFound error, the caller might not know why you were looking for that file, so you should wrap it and say fmt.Errorf("couldn't find config file: %s", err), that way if the user sees the error message, he'll see "couldn't find config file: open: file not found: foo.yml" instead of just "open: file not found: foo.yml".
Flatting the error to a string isn't a good idea, unless you're sure you're dumping it straight out to the user (and by that I mean literal human, not merely "caller"). If you're returning it from a function, you need to wrap it, as in the Rust examples, so the caller can extract the original if they want to do something about it.
Well, so there's two theories there... there's the theory you're saying, that the caller might want to know it's a filenotfound, and handle it programmatically... and there's the theory that the fact that you're doing file I/O is an implementation detail that callers shouldn't depend on, because it could change. What side of that argument you fall on depends a lot on the specifics of your code, and how much you want to sanitize your errors (and how likely you think callers will actually be able to do something programmatically about an error).
I agree with both sides, and what I do in any particular situation depends a lot of on the details of what I'm doing.
No, this doesn't fit either case. If you are going to return the error details in a way the human user can read, then you really ought to just return them symbolically, because a programmer will start running regexes over your error codes to figure out what happened. If you're going to not show the underlying error, then either be willing to entirely hide it (sometimes appropriate, probably not for the usual IO case but there are internal errors for which a translation is appropriate) or wrap and pass up, but don't wrap and pass up as a string. That's the worst of all worlds.
I'm pretty much never going to feel bad about breaking someone's code that runs a regex over the error message I return. I can't stop people from writing bad code. Then again, I'm also careful to write my own code such that consumers of it always have a reasonable way to programmatically detect all the errors I know exist, and anything else is just "something really unexpected went wrong, here's a string to log if you want to".
There's always going to be a category of errors that your code just doesn't understand. Giving the caller a string to log with some context is a lot better than returning just a generic error with no context. Returning the specific error is also possible, and sometimes that may be appropriate, but it can cause just as many problems as the other way... but at least the guy writing a regex should know better, and anyone who reads his code is going to notice it and tell him it's very likely to break. However, if someone type-checks the error you're returning, that looks like pretty valid code, so it's less likely to get red-flagged.... until you change your implementation, and that code now breaks.
I really don't think we disagree terribly much on this btw. Like I said, it depends a lot on the situation. Defining policies for errors is hard. There's always tradeoffs, like anything else.
So it means the client of your library can simply pattern-match on the result returned by cause() to find out if it was caused by, say, FileNotFoundException? Nice.
Nope, cause() returns an &Error, there's nothing you can pattern match there. And despite the documentation's claim, as far as I can see Error does not extend Any, which would be necessary for dynamic typechecking.
Also remember that cause() is recursive, you may need to traverse the whole chain before finding what you're looking for (not that you can find it at the moment)
I don't think that cause() being recursive is an issue, but I'm not sure why you'd want the cause if you can't do anything useful like attempt to downcast it.
You don't need to use cause(). For the LibError example, I think you'd instead want to pattern match against the actual variants defined in the enum, one of which is IoError(io::IoError), at which point you could match the IoError's kind against FileNotFound. This way you're depending on the published contract of the libraries in question, rather than the fact that you'll be given an error whose eventual cause is some file somewhere not being found, which is more of an implementation detail.
That's a good idea in general, but there are times where you do need downcasting. Maybe your generic DB framework can throw a specific error on MySQL that you known how to recover from, but which is only available from cause() but not from the library's contract.
The try! macro is something I know a lot of people would love to have in Go (those pesky if err != nil { return err } lines really bother people for some reason).
I think it's interesting that the built-in error type has a cause, that seems to be one of the most common patterns in Go error packages as well. Is there any automatic stack tracing? That seems to be one other common thing in Go error packages.