I don’t buy the “huge pain” argument. I write lots of Python and Go, and the error boilerplate is a non-issue. I also appreciate that it’s explicit instead of implicit.
It's not just a pain to write. I've accidentally introduced way more bugs through Go style error handling than through Python style error handling. Some examples:
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.
I don’t know man. I have scarcely seen bugs like these and most of them seem pretty conspicuous to me (maybe I’ve just been writing Go long enough), but I see loooads of uncaught Python (and Java) exceptions internally and from high-profile third party tools.
That's very possible. It's very easy to forget try...catch and wind up with uncaught exceptions.
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.
Thanks! It looks like by default it catches the first two. Is there a way to configure it to catch the other two? I don't think it can catch the case where you accidentally return nil because sometimes you do actually want to return nil when you see an error. I also couldn't find any linter that checks that you are using errors.WithStack when needed.
Linting for WithStack seems like it might be tough since you want to make sure the error was annotated exactly once (I think that’s the intended use, anyway?). The linter would need to know whether or not a fallible function call has annotated the error or not. Seems like an interesting exercise in any case.
Having written maybe two lines of Go, many years ago, I guess I'm surprised that all of these cases make it past the type system. Like shouldn't it bitch if you try to return nil when it's expecting a non-nil value or an error? I guess I'll go try to find an online go thinger to find out.
That's not exactly true. All struct types in Go can not be nil. However, they also can't be abstracted over in any way, so they are a poor idea for an error type - you want an error interface, and all interface types in Go indeed are nil-able.
You can abstract over a struct as easily as a pointer, and structs work fine for satisfying interfaces. Specifically, structs make it a little harder for someone to inadvertently mutate things (passed by copy) and because they can’t be nil you don’t need to worry about a non-nil interface implemented by a nil concrete pointer (this is mostly only a problem for people who are new to pointers in my experience). The downside is every time you put a struct into an interface, it gets allocated on the heap, and allocations are expensive in Go (also passing structs can be more expensive than passing pointers).
What I meant about abstraction is that if you define a function which returns an error struct instead of an error interface, you lose any abstraction capability, you can only return that specific struct (and need to find some way of signaling that no error occurred).
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?
> does this just mean liberal amounts of: thing != nil everywhere
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.
> Yes, it does. Whenever you call a function that can possibly fail, you are supposed to add:
if err != nil {
return err
}
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.
I don't know. They're hard for me to find. How would you pick up on the first and last example? They are possibly correct depending on the exact functions you are calling.
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".
Forgetting that a function returns an error:
Forgetting to check the error returned by a function. Note a linter won't pick this up since the err variable is used later. Accidentally typing return nil instead of return err: 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: 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.