Using Error Wrapping
An error often “bubbles up” a call chain of multiple functions. In other words, a function receives an error and passes it back to its caller through a return value. The caller might do the same, and so on, until a function up the call chain handles or logs the error.
An error can be “wrapped” around another error using fmt.Errorf() and the special formatting verb %w.
func ReadFile(path string) ([]byte, error) {
if path == "" {
// Create an error with errors.New()
return nil, errors.New("path is empty")
}
f, err := os.Open(path)
if err != nil {
// Wrap the error.
// If the format string uses %w to format the error,
// fmt.Errorf() returns an error that has the
// method "func Unwrap() error" implemented.
return nil, fmt.Errorf("open failed: %w", err)
}
defer f.Close()
buf, err := io.ReadAll(f)
if err != nil {
return nil, fmt.Errorf("read failed: %w", err)
}
return buf, nil
}
Wrapping the error preserves all this additional information.
Unwrapping Wrapped Errors
An error returned by a function might contain one or more wrapped errors. Printing or logging the received error will also include all error messages from the wrapped errors.
_, err := ReadFile("no/file")
log.Println("err = ", err)
// Unwrap the error returned by os.Open()
log.Println("errors.Unwrap(err) = ", errors.Unwrap(err))
This code snippet prints the following:
Reading a single file: err = open failed: open no/file: no such file or directory
Reading a single file: errors.Unwrap(err) = open no/file: no such file or directory
Testing for Specific Error Types
errors.Is()
Function func Is(err, target error) bool returns true if error err is of the same type as target.
_, err := ReadFile("no/file")
// Prints True
log.Println("err is fs.ErrNotExist:", errors.Is(err, fs.ErrNotExist))
If errA wraps errB errA(errB), errors.Is(errA, ErrB) is true
errors.As()
function As() returns true if err is or wraps an error of the same type as target, and it also unwraps that error and assigns it to target.
target := &fs.PathError{}
if errors.As(err, &target) {
log.Printf("err as PathError: path is '%s'\n", target.Path)
log.Printf("err as PathError: op is '%s'\n", target.Op)
}
Joining Errors
Typically, errors get wrapped one by one while being returned to the respective caller. Sometimes, a function needs to collect multiple errors and wrap them into one.
func ReadFiles(paths []string) ([][]byte, error) {
var errs error
var contents [][]byte
if len(paths) == 0 {
// Create a new error with fmt.Errorf() (but without using %w):
return nil, fmt.Errorf("no paths provided: paths slice is %v", paths)
}
for _, path := range paths {
content, err := ReadFile(path)
if err != nil {
errs = errors.Join(errs, fmt.Errorf("reading %s failed: %w", path, err))
continue
}
contents = append(contents, content)
}
return contents, errs
}
Handling Joined Errors
A joined error is actually a slice of errors, []error.
_, err = ReadFiles([]string{"no/file/a", "no/file/b", "no/file/c"})
log.Println("joined errors = ", err)
e, ok := err.(interface{ Unwrap() []error })
if ok {
log.Println("e.Unwrap() = ", e.Unwrap())
}
A Complete Example
package main
import (
"errors"
"fmt"
"strings"
)
type ConnectionError struct {
Msg string
}
func (e *ConnectionError) Error() string {
return e.Msg
}
type OperationError struct {
Op string
Err error
}
func (e *OperationError) Error() string {
return fmt.Sprintf("Op '%s' Err: %v", e.Op, e.Err)
}
func (e *OperationError) Unwrap() error {
return e.Err
}
func getFullError(err error) string {
var errList []string
for err != nil {
errList = append(errList, err.Error())
err = errors.Unwrap(err)
}
return strings.Join(errList, "\n-->")
}
func main() {
connErr := &ConnectionError{Msg: "Connection Timeout"}
err1 := &OperationError{
Op: "read",
Err: connErr,
}
fmt.Println(err1)
if errors.Is(err1, connErr) {
fmt.Printf("errors.Is: %v\n", connErr)
}
nextErr := errors.Unwrap(err1).(*ConnectionError)
fmt.Println(nextErr)
var nextErr2 *ConnectionError
if errors.As(err1, &nextErr2) {
fmt.Printf("errors.As: %v\n", nextErr2)
}
fmt.Println("-------------------")
err2 := &OperationError{
Op: "write",
// fmt.Errorf() returns an error that has the
// method "func Unwrap() error" implemented.
Err: fmt.Errorf("write failed: %w", connErr),
}
fmt.Println(err2)
curErr := errors.Unwrap(err2)
// Note curErr is not a *ConnectionError
if err, ok := curErr.(*ConnectionError); ok {
fmt.Printf("curErr is a *ConnectionError: %v\n", err)
} else {
fmt.Printf("curErr is not a *ConnectionError: %v\n", curErr)
}
var next2Err *ConnectionError
if errors.As(err2, &next2Err) {
fmt.Printf("errors.As: %v\n", next2Err)
}
fmt.Println("-------------------")
fmt.Printf("getFullError: %v", getFullError(err2))
}