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 errordefer 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.