Pocket Gophers

Handling errors in defer

The reason I didn't do it, is because I don't know how to do it. In essence, that "f.Close()" that you are worried about is wrapped in "defer f.Close" in SaveState in a function. I.e., all is happening during the program tear-down phase. If we are not planting a "log.Fatalln(err)" bomb in SaveState itself, how do you thing we should check it properly?

We have all seen or written a function like:

func process() error {
	f, err := os.Open(filename)
	if err != nil {
		return err
	}
	defer f.Close()

	// some process using f

	return nil
}

This is a process that depends on a resource, f, that needs to be cleaned up. defer keeps the cleanup, f.Close, close to the setup, os.Open, and makes sure the cleanup happens even if the function returns early.

The problem is the error returned by f.Close is ignored.

A Working Example

To understand the errors not being handled, we first need to control the errors that are encountered. The process function can be thought of as only calling three functions: Open, Close, and Process. While a real-world process would use many processing functions, I use Process to indicate if there was an error somewhere during the processing (it is common to return as soon as there is an error in the process).

To control which functions return an error, I created errorOn:

type errorOn struct {
	open    error
	close   error
	process error
}

func (e errorOn) Open() error {
	fmt.Println("Open()")
	return e.open
}

func (e errorOn) Close() error {
	fmt.Println("Close()")
	return e.close
}

func (e errorOn) Process() error {
	fmt.Println("Process()")
	return e.process
}

Setting the fields controls the errors encountered. Displaying which methods were executed makes the difference between an ignored error and a method not being called visible.

The motivating process that ignores the error from Close can then be implemented as:

func process(ocp errorOn) error {
	err := ocp.Open()
	if err != nil {
		return err
	}
	defer ocp.Close()

	err = ocp.Process()
	if err != nil {
		return err
	}

	return nil
}

While this particular function can be simplified by directly returning ocp.Process, the expanded form is a reminder that it takes the place of some more complicated process that returns immediately when an error is encountered. The final return nil is what should happen when there are no errors in the process.

The last part we need to get this example working is to set the errors that will be encountered and execute process:

func main() {
	ocp := errorOn{}

	for _, arg := range os.Args {
		switch arg {
		case "open":
			ocp.open = errors.New("open")
		case "close":
			ocp.close = errors.New("close")
		case "process":
			ocp.process = errors.New("process")
		}
	}

	err := process(ocp)
	fmt.Printf("Error %T: %v\n", err, err)
}

The error type and value are printed to further show what is happening.

Following my practice of exploring alternatives with go run, I put errorOn and main() into main.go. The alternatives we are exploring have different processes. This alternative is in ignored.go.

Get the example code with:

go get -d pocketgophers.com/handling-errors-in-defer

Running this alternative without setting any errors produces:

$ go run main.go ignored.go
Open()
Process()
Close()
Error <nil>: <nil>

We can see that each of the methods was ran and that no error was returned. The first potential error is from Open:

$ go run main.go ignored.go open
Open()
Error *errors.errorString: open

Notice that only Open was ran and the returned error is open ( errors.New(), used to create the error, returns an *errors.errorString). Encountering an error on Process works in a similar way:

$ go run main.go ignored.go process
Open()
Process()
Close()
Error *errors.errorString: process

Next up is encountering an error on Close:

$ go run main.go ignored.go close
Open()
Process()
Close()
Error <nil>: <nil>

Since the returned error is nil, we know that the error on Close was ignored.

Returning a deferred error

defer works on a function or method call.

[…] if the deferred function is a function literal and the surrounding function has named result parameters that are in scope within the literal, the deferred function may access and modify the result parameters before they are returned. If the deferred function has any return values, they are discarded when the function completes. Go Spec.: Defer Statements

Following this recommendation, I updated process to:

func process(ocp errorOn) (rerr error) {
	err := ocp.Open()
	if err != nil {
		return err
	}
	defer func() {
		err := ocp.Close()
		if err != nil {
			rerr = err
		}
	}()

	err = ocp.Process()
	if err != nil {
		return err
	}

	return nil
}

With these changes, the error from Close is no longer ignored:

$ go run main.go close.go close
Open()
Process()
Close()
Error *errors.errorString: close

However, when errors are returned by Process and Close:

$ go run main.go close.go process close
Open()
Process()
Close()
Error *errors.errorString: close

The error from Close overwrites the one from Process.

Returning multiple errors

When both Close and Process return errors, there are two errors. Let’s accept that fact, and generalize it to any number of concurrent errors, by following these rules:

  1. The function must return a named []error, I like to name it errs (plural of err)
  2. To return an error, write: return append(errs, err) (this also works when returning multiple values)
  3. Set errs in deferred functions with: errs = append(errs, err)
  4. Don't append nil errors to errs

Updating our alternative following these rules results in:

func process(ocp errorOn) (errs []error) {
	err := ocp.Open()
	if err != nil {
		return append(errs, err)
	}
	defer func() {
		err := ocp.Close()
		if err != nil {
			errs = append(errs, err)
		}
	}()

	err = ocp.Process()
	if err != nil {
		return append(errs, err)
	}

	return nil
}

Now both errors are returned:

$ go run main.go errors.go process close
Open()
Process()
Close()
Error []error: [process close]

Handling []error

Functions returning []error aren't as common as those returning error. After calling errs := process(ocp), the following expectations can be made about errs:

These expectations can be reduced into the following rules for handling []error:

  1. if len(errs) > 0 to check for errors (assuming none are nil)
  2. for _, err := range errs to handle errors

Returning error instead of []error

Sometimes []erroris needed, but an error must returned instead. There are a few criteria to keep in mind in adapting the previous approach:

There are many ways to make this work. My main goal was to make something that someone familiar with dealing with []error as shown above would not need to work hard to use.

The first thing to do is make an []error that fulfills error.

type multiErr struct {
	Errors []error
}

func (me multiErr) Error() string {
	return fmt.Sprint(me.Errors)
}

Using a struct avoids some runtime type assertions. Directly appending to multiErr.Errs would complicate each time it was appended to, so I implemented an Append function that mirrors the builtin append:

func (me *multiErr) Append(errs ...error) error {
	me.Errors = append(me.Errors, errs...)
	return errors.New("return value for multiErr must be set in the first deferred function")
}

This returns an error so you can replace

return append(errs, err)

with

return errs.Append(err)

The returned error also indicates that something else needs to be done to have a fully working solution.

The last problem is returning the multiErr in a way that err != nil will work. I solve this with

func (me *multiErr) Return(rerr *error) {
	if len(me.Errors) > 0 {
		*rerr = me
	}
}

A deferred call to this function is required at the beginning of your function.

The rules for using multiErr with defer are:

  1. The function must return a named error, I like to name it rerr (for returned error)
  2. At the beginning of the function, create a multiErr, I like to name it errs (plural of err)
  3. The first defer in your function needs to be errs.Return(&rerr). If it is not, you may miss some errors in deferred functions that run after it (they run in the reverse order they were deferred).
  4. To return an error, write: return errs.Append(err) (this also works when returning multiple values)
  5. Set errs in deferred functions with: errs.Append(err)
  6. Don't Append nil errors to errs

Following these rules, process is updated to:

func process(ocp errorOn) (rerr error) {
	var errs multiErr
	defer errs.Return(&rerr)

	err := ocp.Open()
	if err != nil {
		return errs.Append(err)
	}
	defer func() {
		err := ocp.Close()
		if err != nil {
			errs.Append(err)
		}
	}()

	err = ocp.Process()
	if err != nil {
		return errs.Append(err)
	}

	return nil
}
$ go run main.go multierr.go
Open()
Process()
Close()
Error <nil>: <nil>
$ go run main.go multierr.go process
Open()
Process()
Close()
Error *main.multiErr: [process]
$ go run main.go multierr.go process close
Open()
Process()
Close()
Error *main.multiErr: [process close]

This is almost as elegant as using []error, but conforms to the interface and expectation of error.

Is there a package I can import?

Yes. github.com/hashicorp/go-multierror:

package main

import (
	"github.com/hashicorp/go-multierror"
)

func process(ocp errorOn) (rerr error) {
	err := ocp.Open()
	if err != nil {
		return multierror.Append(rerr, err)
	}
	defer func() {
		err := ocp.Close()
		if err != nil {
			rerr = multierror.Append(rerr, err)
		}
	}()

	err = ocp.Process()
	if err != nil {
		return multierror.Append(rerr, err)
	}

	return nil
}

multierror.Append does more work than multiErr.Append, but has the benefit of not needing the deferred function to set rerr.

$ go run main.go go-multierror.go
Open()
Process()
Close()
Error <nil>: <nil>
$ go run main.go go-multierror.go process
Open()
Process()
Close()
Error *multierror.Error: 1 error occurred:

* process
$ go run main.go go-multierror.go process close
Open()
Process()
Close()
Error *multierror.Error: 2 errors occurred:

* process
* close

I was tempted to only call multierror.Append in the deferred function because that is the only place in this example code that can have multiple errors:

func process(ocp errorOn) (rerr error) {
	err := ocp.Open()
	if err != nil {
		return err
	}
	defer func() {
		err := ocp.Close()
		if err != nil {
			rerr = multierror.Append(rerr, err)
		}
	}()

	err = ocp.Process()
	if err != nil {
		return err
	}

	return nil
}
$ go run main.go go-multierror-defer-only.go
Open()
Process()
Close()
Error <nil>: <nil>
$ go run main.go go-multierror-defer-only.go process
Open()
Process()
Close()
Error *errors.errorString: process
$ go run main.go go-multierror-defer-only.go process close
Open()
Process()
Close()
Error *multierror.Error: 2 errors occurred:

* process
* close

This is fine if the errors are only printed out or logged, but if the code needs to respond to an error returned by Process, the error handling code will need two paths to get to that error:

  1. As an error returned directly
  2. As an error contained in multierror.Error

Always using multierror.Append eliminates the first case. The second case cannot be avoided without ignoring an error.

Summary

  1. Name the return value(s) so they can be set in a deferred function.
  2. Use []error if possible.
  3. If the function must return an error, use multierror.Append everywhere you return an error or set the error(s) to be returned.

Dig into the Fundamentals of Go

Subscribe to receive a weekly email covering a Go fundamental. Be it the language, its tooling, or its packages, you will learn what you need to know.