Go Defer Errors

16 Aug 2021

What if you want to return an error from a function, and that function has a defer that could also return an error?

First, let's look at the usual case, where we really don't care about the error returned by the defer:

package main

import (
	"fmt"
	"log"
)

func Rollback() error {
	return fmt.Errorf("Could not roll back")
}

func GetName(id int) (string, error) {
	defer Rollback()
	return "Manni", nil
}

func main() {
	name, err := GetName(42)
	if err != nil {
		log.Fatalf("Error trying to get name: %v", err)
	}
	log.Printf("Name is: %v", name)
}

Running the above code produces:

2021/08/16 20:45:05 Name is: Manni

If we were interested in returning that error from the defer, we could use named return variables, so that return variable err could get assigned to by Rollback().

package main

import (
        "fmt"
        "log"
)

func Rollback() error {
        return fmt.Errorf("Could not roll back")
}

func GetName(id int) (name string, err error) {
defer func() { err = Rollback() }()
return "Manni", err } func main() { name, err := GetName(42) if err != nil { log.Fatalf("Error trying to get name: %v", err) } log.Printf("Name is: %v", name) }

Running the above code produces:

2021/08/16 20:50:28 Error trying to get name: Could not roll back

Of course, if GetName() called a SQL library that could also return an error, we would be interested in that error too; in fact, likely more interested. (In fact, usually a rollback error can just be ignored: our example here is rather contrived. But think of a situation where closing a file happens in a defer, and you are afraid of running out of file handles. Then, the defer error might be as interesting as the "main" error!)

Here is a version of our program where we capture both the "main" error (which comes from executing a SQL statement) and a defer error. There are a few ways to handle this, and I have decided to create a new error type called multiError. It has an Error() method to implement Go's built-in error interface, and it has an Unwrap() method to work with Go's newer errors.Unwrap(), not to mention errors.Is() and errors.As(). It also introduces a Main() method for getting the "main" error (as opposed to the defer error).

package main

import (
        "fmt"
        "log"
)

// multiError holds a main error and a subordinate error type multiError struct { err error subErr error } func (e *multiError) Error() string { return fmt.Sprintf("%s --- ALSO: %s", e.err.Error(), e.subErr.Error()) } func (e *multiError) Main() error { return e.err } func (e *multiError) Unwrap() error { return e.subErr } func RunSQLQuery(id int) (string, error) { return "", fmt.Errorf("Malformed SQL statement") }
func Rollback() error { return fmt.Errorf("Could not roll back") } func GetName(id int) (name string, err error) {
var qErr error
defer func() {
rErr := Rollback() switch { case qErr != nil && rErr != nil: err = &multiError{err: qErr, subErr: rErr} case qErr != nil: err = qErr case rErr != nil: err = rErr }
}()
name, qErr = RunSQLQuery(id)
return "Manni", err } func main() { name, err := GetName(42) if err != nil { log.Fatalf("Error trying to get name: %v", err) } log.Printf("Name is: %v", name) }

Running the above code produces:

2021/08/16 22:12:43 Error trying to get name: Malformed SQL statement --- ALSO: Could not roll back

Our switch statement in the above code deals with any combination of SQL error and rollback error being non-nil. You can play with RunSQLQuery() and Rollback() (not) returning errors to see what the various outputs of the above program are.

There are other ways of dealing with this too. One way is to return a slice of errors instead of a nested error as I have done here. There is no "one true way": it depends on your requirements.