Just recently, I was reminded how hard correct error handling is, while using the Go client for Google Cloud Platform.

The code I was writing looked like this:

Everything seemed fine, except my-object was not created!

Inspecting the whole thing a bit closer, it turned out that writer.Close() (line 2) returned an error saying that the client is not authorized.

My first reaction was, that gcpClient.Bucket(“my-gcp-bucket”).Object(“my-object”).NewWriter(context.TODO()) or io.Copy(writer, src) should return that error already. But giving it some more thought, I realized that this isn’t possible, and furthermore, it’s perfectly fine for an I/O object to return an error only when closing it.

The point is, an I/O object can return an error either (1) while acquiring it, (2) while reading from/writing to it, or (3) while closing it. This is part of the contract.

So the often used defer ioObject.Close() relies on the assumption that errors returned from Close() can be ignored. This isn’t true in general. Examples being the writer from the example above, or even a simple os.File .

From https://linux.die.net/man/2/close:

Not checking the return value of close() is a common but nevertheless serious programming error. It is quite possible that errors on a previous write(2) operation are first reported at the final close(). Not checking the return value when closing the file may lead to silent loss of data.

Unfortunately, The Go blog — Defer, Panic, and Recover recommends exactly that:

As you can see, none of the Close() errors are handled.

Correct Error Handling

What would a version of this look like with correct error handling? Like this:

What changed? We removed the defer calls and replaced them with explicit Close calls in all code paths. Now, when an error happens during Copy (line 13) we return an error, but close the two files before (lines 15–17). We ignore the error from Close because this either succeeds or it fails, but at that point it might be just a consequence of the previous error that has already happened. After the copying is done, we close the files, but explicitly handle the errors from those calls and do not ignore them.

Is this verbose? Yes, it is! When you handle errors correctly, code becomes verbose. It’s also your lifeline when something in production really goes wrong.

Unfortunately, this code has some issues as well:

it contains a lot of duplicate lines

it’s difficult to get the Close call logic right

call logic right it won’t close files when a panic happens somewhere

Can we do better?

The SafeCloser Approach

It turns out a little helper, that I call SafeCloser , can help:

It’s really just a safe-guard so you want close a Closer multiple times. A lot of Closer implementations are already idempotent, but there is no guarantee. The SafeCloser is that guarantee.

How does it work in action? For every Closer you simply create a companion SafeCloser and close your Closer through this SafeCloser .

Example for our CopyFile function:

Note how I declared a SafeCloser before every defer block and used in there (lines 6, 7, 13, 14). That same SafeCloser is then used at the bottom (lines 21 and 26) when we explicitly close the files.

We avoided the duplicate lines and panics are handled correctly.

I deliberately did not implement the io.Closer interface with the SafeCloser to avoid misuse. It is not intended to wrap any io.Closer , pass it around, and be lax about closing them several times. Instead, it’s for this very specific use case, where you handle Close calls differently depending on if you are in failure mode or in success mode.

Summary

Correct error handling is hard. Most often it is not done correctly. A little helper like the SafeCloser from above can help make your error handling correct, while keeping it as simple as possible.

If you liked this post, you may also be interested in my follow-up post, “Correct Error Handling is Hard, Part 2".