5 ms·
In my experience, the code inside the `if err != nil` branch is often different. At least, when I write code. It takes me longer to write Go code because of al
by klodolph 4y ago
In my experience, the code inside the `if err != nil` branch is often different. At least, when I write code.
It takes me longer to write Go code because of all the error handling stuff I'm putting in. Then it takes me less time to debug and fix run-time errors, because the error messages that my code generates are informative. I like this tradeoff for production code. This tradeoff is usually a bad tradeoff for throwaway scripts.
I have plenty of experience in other languages, including languages with exceptions and languages with enforced error checking (like Rust). My experience with these other languages has made Go feel a less clumsy.
For languages with exceptions, I often end up with code like this:
SomeType aVar;
try
{
aVar = SomeMethod();
}
catch (SomeError ex)
{
throw new OtherError("description", ex);
}
For languages like Rust, I often end up with this:
let aVar = match SomeFunction() {
Ok(x) => x,
Err(err) => return Err(OtherError::EnumValue(someContext, err)),
};
(Yes, I know about map_err.)
And in Go, I end up with:
aVar, err := SomeFunction()
if err != nil {
return nil, &Error{"some context", err}
}
This is not meant as a statement about “the correct” way to write code, it’s just meant as an illustration of the kind of code I often end up with, in projects that I work on.
It depends on how you write code, what you're doing, and your approach to solving errors. Do note that you don’t HAVE to use the if err != nil approach in all of your Go code. It is not the only method for bubbling errors up. There’s a whole discussion to be had about when it is appropriate to panic()/recover() and why you would choose to panic() in Go, even though people say you’re “not supposed to do that”.
- steveklabnik 4y ago> (Yes, I know about map_err.) I am curious to hear more about why you prefer what you wrote to map_err, if you're willing to share. I have my own guess, but I'm always interested in things like this. (For those that don't know, you could write the above code as something like: let aVar = SomeFunction().map_err(|err| OtherError::from(someContext, err))?; or if it becomes too long, let aVar = SomeFunction() .map_err(|err| OtherError::from(someContext, err))?; and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense.)
- klodolph 4y agoThe difference with .map_err() is that you are no longer in the control flow of the parent function, so you can't decide to ignore the error inside the .map_err(), and you can't decide to return/break there either. You end up combining .map_err() with ?. The "match" syntax is very simple, clear, and flexible. I don't have to remember the different Result methods: or(), map_err(), map_or_else(), and_then(), etc. There are just too many methods to remember, and match does everything. (Edit: and it's easier to borrow or move something from the enclosing scope inside 'match'.) If I later decide that the Err() branch inside the match needs to do something other than propagate an error, I can just change the code there, without having to change the .map_err() to something else. I like match because it's simple, easy to read, easy to write, and flexible. In Haskell, I’m more comfortable using functions like Rust's map_err because I can, e.g., put it on the outside of a "do" block, and in Haskell, it’s not like you’re going to "break" or "return" anyway (not in the imperative sense). In other words, in Haskell, something like mapError has the same power as "case" does (and perhaps more, since there are multiple ways to propagate errors in Haskell, and mapError will handle all of them, as long as you’re in an error-handling monad).
- steveklabnik 4y agoCool thank you! That is actually not the thing that I would have guessed, which is why I love asking these questions rather than assuming. > you are no longer in the control flow of the parent function, Yeah, I run into this myself from time to time. I still end up defaulting to map_err when possible and then doing this when I have to, personally, but I can see the logic here. > I don't have to remember the different Result methods: I 1000% understand this. I think where I'm personally at is because I do know those methods, I end up doing the opposite: it takes me a second to go "oh, this is just map_err," which is the same problem but in reverse, and someone's gonna have to do it one way or the other.
- anyfoo 4y ago> and in some cases you don't even need the |err| but since we're taking two arguments here you can't 'curry' it in a sense. This is extremely pedantic, but without knowing Go, it looks like it's the other way around: Because you cannot curry the function "from"[1], you cannot do the partial application of "from" to "err" (which after currying would not really be a "partial application" but just application of the outermost function). Hence usage of what looks like a lambda. [1] I guess currying is just not a thing in Go, because this looks like a straightforward case from a distance?
- tsimionescu 4y agoIn my experience, the majority of the time, all you want to do with an error is to pass it on to the function above, while adding some amount of context to be able to later tell where it appeared in this function. Exceptions with stack traces (Java, C#, Python, Common Lisp - really anything except C++) give you both things for free: control flow stops, the function calling you gets the error, and information about where the error occurred is passed on. At component boundaries you typically catch the error and perhaps convert it into a different type; and somewhere close to the user interface level you typically actually handle it in some meaningful way (retry operation, show error to user, cancel other work etc). Here by user interface I don't mean just GUI, but things like HTTP request handlers, terminal UI, RPC etc. In most codebases that I've seen, error handling (as opposed to bubbling the error up with a bit of added context) is perhaps 1% of cases - whether in Java or Go or C. And in Java, this just means that 99% of cases will "handle" an error by simply calling a function, or perhaps the try-with-resources statement.
- klodolph 4y agoI find stack traces to contain more noise and less signal than the error messages I see in Go. So the idea that exceptions with stack traces give you “both things for free” doesn’t match my experience. I have professional experience in both software engineering and operations for apps written in both styles—from my experience, using exceptions makes it easier to write the code, and harder to diagnose errors in the running application.
- tsimionescu 4y agoSure, well crafted error traces will always beat automatically generated stack traces. However, in my experience debugging both local and customer issues based on logs, exceptions are, say, 80% of the way there, and they neither pollute your code nor leave you with 0 information if someone was lazy or in a hurry to ship something. Personally, I feel that what exceptions with stack traces get you is going to be the best trade-off between nice error messages in logs VS amount of effort in code spent to detail error paths. Of course, this doesn't have to be a 100% trade-off, and exceptions still allow you to add extra context whenever you feel it is necessary. Even if you decide to do it 100% of the time, they still act as an excellent safety net, making sure that you can't forget about an error, and need not be more verbose than if err!=nil checks or matches on the Result type (though I admit they do tend to be more verbose in most languages, requiring an extra block for try to be introduced).
- lamontcg 4y agoThat is at the 'edge' of your code where you are calling some function and needing to translate to your code's error type. At all the intermediate layers of functions in languages with exceptions you write no code and just let it bubble up, while in Rust you just use `?` to send the error back up a level, while in Go you have to be verbosely explicit around every function call at every level.
- ilyt 4y agoIt would be still nice do "return if error" in one line say via some "macro" (I'd kill for Rust-like macros in go...) err!(err,"Connection to %s failed",serverAddr") or even return ErrConnFailed{address,err} if err != nil instead of if err != nil { return "",nil,Struct{},fmt.Errorf("Connection to %s failed: %s",serverAddr,err) } You can't even shorten it to one line because go fmt will "helpfully" expand it back to 3 lines... I do think writing context (whether by error type or "just" text) with each error is the way to go vs "just throw it up the stack and hope for best" but Go syntax fails it in that quest.
- alkonaut 4y agoThe difference between those 3 examples is that the first two lean on the compiler for some nice rewards and the last one on the developer remembering to check the err variable. Syntax wise it’s not much worse (until you start having multiple possible errors in a sequence when its ugliness scales linearly), but the problem is that you pay the 4-5 lines of boilerplate for zero rewarded guarantees such as “if the first thing failed the second can’t/won’t be attempted”. It feels like a lot of syntax cost and boilerplate boredom without much reward.