Forgetting that a function returns an error:
...
foo() // foo returns an error that isn't being handled.
...
Forgetting to check the error returned by a function. Note a linter won't pick this up since the err variable is used later. ...
err := foo()
err = bar() // The previous error will go unhandled.
...
Accidentally typing return nil instead of return err: ...
if err != nil {
return nil
}
...
And in the case of the errors library, there's times where I will call a builtin function that returns an error and forget to call errors.WithStack. Every once in a while I'll come across an error without a stack trace and I'll have to hunt down where it came from: ...
err := json.Unmarshal(bytes, &obj)
if err != nil {
return err // should be errors.WithStack(err)
}
...
All of these issues look just like normal bug free Go code. On the basis that I've introduced more bugs this way, I prefer Python style error handling by far.Not to me.
The other two examples, I could maybe understand, but they still look pretty close to normal Go code. In the case where you forget to handle an error, you need to be able to recognize the absence of the error checking bit. It would be one thing if there was extra code that looked wrong, but looking for the absence of code makes it hard to spot.
In the case where you return nil, it looks exactly like a normal early-exit from a function. You need to be able to recognize that the code is not a normal early-exit, and that the three letters "err" were swapped for the three letters "nil".
And... Woah. That's kind of horrifying to me (though I totally get the explicitness of it)... does this just mean liberal amounts of: thing != nil everywhere? Or is the rest of the memory management I guess, uhh.., good/magical enough you don't have to worry about it constantly in calls further down the stack if you've checked it once? Or are you always feeding the nil check beast?
Yes, it does. Whenever you call a function that can possibly fail, you are supposed to add:
if err != nil {
return err
}
You can think of it as unwinding the stack by hand. That's why a lot of people complain about Go error handling so much. That and a lack of generics. Looking at some code I've written, about 15-20% of the lines in a file are responsible for error handling.> Or is the rest of the memory management I guess, uhh.., good/magical enough you don't have to worry about it constantly in calls further down the stack if you've checked it once?
I don't quite understand the question here. Are you asking about a performance impact of having nil checks everywhere? If I had to guess, I would think there's a negligible performance impact because the branch predictor will always predict the happy case. As for memory concerns, as long as you are in the happy case and returning nil, no memory needs to be allocated. It's the same reason null doesn't require any memory allocation in other languages.
I don’t think that’s what the parent meant by his question. He was asking if you have to add runtime checks all over to make sure any reference type isn’t nil; no, you don’t—you only add the checks for those for which nil is a valid state in the program (such as errors). If it’s an invalid state, it will panic because it’s an exceptional circumstance—a programmer error. This isn’t a defense of nil; only a clarification about how nil is dealt with. With this context, the rest of his question (the part you didn’t understand) becomes clearer.
Honestly though, I would prefer the uncaught exception case. When an uncaught exception is thrown, it's very clear you have an uncaught exception and you know exactly where it came from. In the examples I wrote, you will accidentally catch an error silently. You will never know if anything went wrong unless silently catching the error triggers an issue somewhere else. Even then, it's pretty hard to trace back the bug to the lack of error handling code somewhere else.