A common gripe I’ve had with Go is that the mantra is “you should handle
errors”, but at the same time the ergonomics of handling errors from
(io.ReadCloser).Close() in a defer call is cumbersome. But fear no more!
With the Go 1.20 release, there’s a nifty way to handle this with the new
errors.Join.
Let me first explain a bit about the issue. Usually if you want to do the
“right” thing and check for errors in a defer call, you end with something
like this:
func example(r io.ReadCloser) (err error) {
// We need this big anonymous function that takes up a bunch of
// precious lines...
defer func() {
// Ugghh, need to check for the error and then set it D:
cerr := r.Close()
if cerr != nil {
err = cerr
}
}
// ... code that reads from r ...
}
This approach has a shortcoming. If the err variable was set
before the defer function is called we’ll override the original error with our
(io.ReadCloser).Close() call error.
Even worse, if you’re lazy and you figured the error check is not worth the amount of work, you end up with just ignoring the error altogether:
func example(r io.ReadCloser) (err error) {
defer r.Close() // Oh no, we'll never know if this errors :(
// ... code that reads from r ...
}
This is pretty common among the codebases I’ve seen (and I’m also an offender of this). It’s not really the proper way to do things. You should either handle the error, or at least log it so you’re aware of that there are issues closing the reader for whatever reason.
Luckily we now have errors.Join 🤓
With the Go 1.20 release, you can now join errors so that you don’t override the
original error with the (io.ReadCloser).Close() error and not need to make the
repetitive if err != nil check all the time. The new function
errors.Join will only return errors that are
non-nil. And if all are nil, it’ll of course return nil. This perfectly
fits the use case of handling close errors in a defer! 💥
Let’s create a new anonymous defer function, and just pass along our original
err and the (io.ReadCloser).Close() error:
func example(r io.ReadCloser) (err error) {
defer func() {
err = errors.Join(err, r.Close()) // Magic!
}()
// ... code that reads from r ...
}
Psst, here’s a full working example: https://go.dev/play/p/J-rkdh0jYme
Now, if any of the errors occur, the err will be set. In the case where both
errors, we get a new error where they are joined with a \n delimiter:
origErr := errors.New("original error")
closeErr := errors.New("close error")
joinedErr := errors.Join(origErr, closeErr)
fmt.Println(joinedErr)
// Output:
// original error
// close error
You can even take it one step further and get rid of the anonymous function by
defining a joinErrs(...) that takes a pointer to the original error. It’s
important that we pass the close function and invoke it within the
joinErrs(...) function because defer will evaluate the parameters directly
(thanks to abhinavg for pointing this out[1]).
So, we end up with something like this:
func joinErrs(origErr *error, fn func() error) {
*origErr = errors.Join(*origErr, fn())
}
func example(r io.ReadCloser) (err error) {
defer joinErrs(&err, r.Close) // Woho! Only a single line :D
// ... code that reads from r ...
}
Wow! We just saved two (!!) lines of code everytime we need to defer a
(io.ReadCloser).Close() call.
And here’s a working example of this: https://go.dev/play/p/sEMxcWYHKBK
TL;DR
You can set errors from defer calls with
errors.Join:
func example(r io.ReadCloser) (err error) {
defer func() {
err = errors.Join(err, r.Close()) // Magic!
}()
// ... code that reads from r ...
}
Errata
- [1]: abhinavg kindly pointed out on Reddit
that an earlier version of this post made the mistake of passing
r.Close()instead ofr.Closeas an argument tojoinErrs(...). Becausedeferwill evaluate the parameters immediately when calling theexamplefunction it would erroneously close the reader even before reading from it. See: https://www.reddit.com/r/golang/comments/11luvxm/comment/jbeb92w/