I find Go’s error handling refreshingly simple & clear. Nowadays, all I do is wrap my errors using fmt.Errorf
with some additional context. The pattern I tend to use most of the time just includes a function/method name, like so:
func someFunction() error {
err := someStruct{}.someMethod()
return fmt.Errorf("someFunction: %w", err)
}
type someStruct struct {
}
func (ss someStruct) someMethod() error {
return fmt.Errorf("someMethod: %w", errors.New("boom"))
}
If I now call someFunction
, the error - when logged or printed - will look like this: someFunction: someMethod: boom
. This gives just enough context to figure out what happened, and where.
I thought to myself - this can be wrapped in a helper function, to automatically take the caller name and do this wrapping for me.
Here’s an implementation:
func WrapWithCaller(err error) error {
pc, _, _, ok := runtime.Caller(1)
if !ok {
return fmt.Errorf("%v: %w", "unknown", err)
}
fn := runtime.FuncForPC(pc).Name()
pkgFunc := path.Base(fn)
return fmt.Errorf("%v: %w", pkgFunc, err)
}
We can now use it like so:
func someFunction() error {
err := someStruct{}.someMethod()
return WrapWithCaller(err)
}
type someStruct struct {
}
func (ss someStruct) someMethod() error {
return WrapWithCaller(errors.New("boom"))
}
And it will return the following: main.someFunction: main.someStruct.someMethod: boom
. Keep in mind that this will have performance overhead:
func Benchmark_WrapWithCaller(b *testing.B) {
err := errors.New("boom")
for i := 0; i < b.N; i++ {
_ = WrapWithCaller(err)
}
}
// Result:
// Benchmark_WrapWithCaller-10 2801312 427.0 ns/op 344 B/op 5 allocs/op
func Benchmark_FmtErrorf(b *testing.B) {
err := errors.New("boom")
for i := 0; i < b.N; i++ {
_ = fmt.Errorf("FmtErrorf: %w", err)
}
}
// Result:
// Benchmark_FmtErrorf-10 13475013 87.19 ns/op 48 B/op 2 allocs/op
So it’s 4-5 times slower than a simple wrap using fmt.Errorf
.
One benefit of using WrapWithCaller
is that refactoring is easier - you change the function/method name and don’t need to bother yourself with changing the string in the fmt.Errorf
. The performance hit is likely not an issue for most programs, unless you’re wrapping tons of errors in hot loops or something like that.
I’m a bit torn if I want to add this helper to my codebases and use it everywhere - seems like an extra dependency with little benefit, but I’ll have to give it a spin before I decide if this is something I’m ready to accept. Time will tell. Thanks for reading!