Wrapping multiple errors in Go 1.20

Peter Gillich
Dev Genius
Published in
3 min readFeb 18, 2023

--

Error handling of Go standard library is dummy. The new function errors.Join wraps multiple errors, which is a simple way to combine a library error message with own error message. This article shows how it works in the reality.

The errors.Join is introduced in Go 1.20, see more details at Go 1.20 Release Notes, Wrapping multiple errors.

Sample code

The error handling will be shown with a typical use case: there is a database layer, which returns several errors but own source code would wrap it to own error: ErrDaoError, so the other part of own source code can decide simple way is the error a database error or other (for example: filesystem error).

Let’s see the baseline:

package dao

import (
"errors"

"github.com/glebarez/sqlite"
"gorm.io/gorm"
)

var ErrDaoError = errors.New("database error")

type Product struct {
gorm.Model
Code string `gorm:"uniqueIndex"`
Price uint
}

func InitDB() *gorm.DB {
var db *gorm.DB
var err error
if db, err = gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}); err != nil {
panic("failed to connect database")
}
if err = db.AutoMigrate(&Product{}); err != nil {
panic("failed to migrate database")
}

return db
}

func GetProduct(db *gorm.DB, id uint) (*Product, error) {
product := &Product{}
if err := db.Take(product, id).Error; err != nil {
return nil, err
}

return product, nil
}

Unwrap

Prior to Go 1.20, The errors.Unwrap function and interface can be used to wrap multiple errors, but its usage is not simple, because several Go interfaces must be implemented to support the errors.Is function:


type ErrWrap struct {
err error
werr error
}

func NewErrWrap(err error, werr error) error {
return &ErrWrap{err, werr}
}

func (ew *ErrWrap) Error() string {
return fmt.Sprintf("%s: %s", ew.err, ew.werr)
}

func (ew *ErrWrap) Unwrap() error {
return ew.werr
}

func (ew *ErrWrap) Is(target error) bool {
return errors.Is(ew.err, target) ||
errors.Is(ew.werr, target)
}

The NewErrWrap wraps own and the database error, see an example:

// GetProductErrWrap uses own error wrapper (ErrWrap)
func GetProductErrWrap(db *gorm.DB, id uint) (*Product, error) {
product := &Product{}
if err := db.Take(product, id).Error; err != nil {
return nil, NewErrWrap(ErrDaoError, err)
}

return product, nil
}

Own ErrWrap type can be tested by the errors.Is function, for example:

package dao

import (
"errors"
"testing"

"github.com/stretchr/testify/assert"
"gorm.io/gorm"
)

func TestGetProductErrWrap(t *testing.T) {
db := InitDB()

product, err := GetProductErrWrap(db, 100)
assert.Error(t, err)
if !errors.Is(err, ErrDaoError) {
t.Error("Not ErrDaoError")
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
t.Error("Not ErrRecordNotFound")
}
assert.Equal(t, "database error: record not found", err.Error())
assert.Nil(t, product)
}

It works well, but we have to define ErrWrap type.

Join

The errors.Join wraps the two errors native way, so own type (ErrWrap) won’t have to be defined, see an example:

// GetProductErrJoin does not use own error wrapper (ErrWrap)
func GetProductErrJoin(db *gorm.DB, id uint) (*Product, error) {
product := &Product{}
if err := db.Take(product, id).Error; err != nil {
return nil, errors.Join(ErrDaoError, err)
}

return product, nil
}

The joined error type can also be tested by the errors.Is function, for example:

func TestGetProductErrJoin(t *testing.T) {
db := InitDB()

product, err := GetProductErrJoin(db, 100)
assert.Error(t, err)
if !errors.Is(err, ErrDaoError) {
t.Error("Not ErrDaoError")
}
if !errors.Is(err, gorm.ErrRecordNotFound) {
t.Error("Not ErrRecordNotFound")
}
assert.Equal(t, "database error\nrecord not found", err.Error())
assert.Nil(t, product)
}

Please note, the Error function concatenates the error strings by \n character, instead of : character. If the : is needed, it is possible to unwrap the list of wrapped errors by the Unwrap() []error function and concatenating by own way, for example:

 if errs, is := err.(interface{ Unwrap() []error }); is {
parts := make([]string, len(errs.Unwrap()))
for e, err := range errs.Unwrap() {
parts[e] = err.Error()
}
assert.Equal(t, "database error: record not found", strings.Join(parts, ": "))
} else {
t.Error("Not wrapped error")
}

Summary

The errors.Join function gives a more simple way to wrap errors. If the format of Error function result is not good, the error strings of Unwrap() can be concatenated by own way.

--

--