Wrapping Errors with Defer
I wanted to document a small pattern I use to avoid repetition in error handling in Go while still providing context as an error climbs up the call stack. I first became aware of this pattern back in 2018 while the Go team was discussing error handling proposals. I’m not exactly sure who first had this idea; the earliest example of it I can find is in a comment on a gist, but I’m fairly certain that’s not where I first saw it. (If anyone knows, please send me an email!)
In short, if you have code like this:
func foo(x string) (*MyStruct, error) {
if err := doSomething(); err != nil {
return nil, fmt.Errorf("foo %s: %v", x, err)
}
if err := doSomethingElse(); err != nil {
return nil, fmt.Errorf("foo %s: operation 2: %v", x, err)
}
return new(MyStruct), nil
}
You can rewrite it like this:
func foo(x string) (_ *MyStruct, err error) {
defer func() {
if err != nil {
err = fmt.Errorf("foo %s: %v", x, err)
}
}()
if err := doSomething(); err != nil {
return nil, err
}
if err := doSomethingElse(); err != nil {
return nil, fmt.Errorf("operation 2: %v", err)
}
return new(MyStruct), nil
}
If you only have one or two spots in your function where you’re wrapping the error, then this approach is probably overkill. However, if you have a lot of early returns on error, this approach can avoid the repetition of constructing the error message. (I even encountered the annoyance of fixing the multiple return statements while drafting this post!)
If you’re confused as to how this works,
let’s refresh our memory on how defer statements in Go behave.
Deferred function calls are executed in reverse order after a function returns or panics,
but before control flow returns to the function that called the function that the defer statement appears in.
As per the specification, “deferred functions are executed after any result parameters are set by that return statement
but before the function returns to its caller.”
So in this pattern, because we name the error result parameter err,
then each return statement will implicitly assign to err
and its value will be observable inside the deferred closure.
The closure then wraps any non-nil error using fmt.Errorf in the usual way.
Another lesser-known part of Go is that the use of named result parameters does not require you to name all the result parameters. The Go specification explicitly allows using the blank identifier in lists of parameters or results.
And a final note: when I’ve presented this pattern to other Go programmers,
their first reaction is usually something like “I don’t like using named result parameters because they’re confusing.”
Usually when I ask some follow-up questions,
I find that their avoidance comes from the so-called “bare” return statement form,
where the expression list can be omitted.
I agree: I recommend always writing out the return values explicitly.
That said, named result parameters in Go have useful semantic and documentation benefits,
and I think using them properly can assist in writing code that is easier to maintain.
EDIT 2026-07-29: Updated the example to demonstrate how individual returns can include more details.