errs -- Error handling for Golang
Package errs implements functions to manipulate error instances. This package is required Go 1.20 or later.
Migrated repository to github.com/goark/errs
- Wrap any
errorand collect context at the point of failure - Add arbitrary key/value context with
WithContext - Include caller function name in context by default
- Print structured error data with
%+v(JSON-like output) - Handle multi-errors in a concurrency-safe way via
errs.Errors
- Go 1.20 or later
- Task command
task test
task govulncheck
Run all maintenance tasks:
task
ci: lint (golangci-lintwithgosec), tests, andgovulncheckCodeQL: scheduled and push/PR static analysis
All sample files under sample/ use the run build tag.
go run -tags run ./sample/sample1.go
- New error with cause and context: sample/sample1.go
- Wrap existing error with context: sample/sample2.go
- Wrap sentinel error with cause: sample/sample3.go
- Multiple causes with
errors.Join: sample/sample4.go - Multi-error with
errs.Join: sample/sample5.go - Concurrency-safe multi-error accumulation: sample/sample6.go
- Zap structured logging object example: zapobject/example_test.go
%v: human readable error message%#v: Go-syntax-like internal structure%+v: structured JSON-like representation
errs.Is, errs.As, and errs.Unwrap are thin wrappers around errors.Is, errors.As, and errors.Unwrap.
errs.Unwraps returns []error and works for both single-cause and multi-cause errors.
For a single cause, it returns a one-element slice.
For multiple causes, it returns all causes as a slice.
errs.EncodeJSON serializes generic error values by traversing unwrap chains when possible.
errs.New("")returnsnilerrs.Wrap(nil)returnsnil- If
WithCauseis given multiple times, the last cause is used errs.Join(...)ignoresnilarguments and returnsnilif all arguments arenil
errs.Errorsis goroutine-safe for container operations such asAdd,ErrorOrNil, andUnwrap.- Errors stored in
errs.Errorsare not guaranteed to be goroutine-safe. errs.Errorhas mutable state (Contextmap), so avoid concurrent mutation while formatting or encoding the same instance.
package main
import (
"fmt"
"os"
"github.com/goark/errs"
)
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.New(
"file open error",
errs.WithCause(err),
errs.WithContext("path", path),
)
}
defer file.Close()
return nil
}
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory
fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}}
fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}}
}
}package main
import (
"fmt"
"os"
"github.com/goark/errs"
)
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.Wrap(
err,
errs.WithContext("path", path),
)
}
defer file.Close()
return nil
}
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%v\n", err) // open not-exist.txt: no such file or directory
fmt.Printf("%#v\n", err) // *errs.Error{Err:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Cause:<nil>, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}}
fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"}}
}
}package main
import (
"errors"
"fmt"
"os"
"github.com/goark/errs"
)
func checkFileOpen(path string) error {
file, err := os.Open(path)
if err != nil {
return errs.Wrap(
errors.New("file open error"),
errs.WithCause(err),
errs.WithContext("path", path),
)
}
defer file.Close()
return nil
}
func main() {
if err := checkFileOpen("not-exist.txt"); err != nil {
fmt.Printf("%v\n", err) // file open error: open not-exist.txt: no such file or directory
fmt.Printf("%#v\n", err) // *errs.Error{Err:&errors.errorString{s:"file open error"}, Cause:&fs.PathError{Op:"open", Path:"not-exist.txt", Err:0x2}, Context:map[string]interface {}{"function":"main.checkFileOpen", "path":"not-exist.txt"}}
fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"file open error"},"Context":{"function":"main.checkFileOpen","path":"not-exist.txt"},"Cause":{"Type":"*fs.PathError","Msg":"open not-exist.txt: no such file or directory","Cause":{"Type":"syscall.Errno","Msg":"no such file or directory"}}}
}
}package main
import (
"errors"
"fmt"
"io"
"os"
"github.com/goark/errs"
)
func generateMultiError() error {
return errs.New("error with multiple causes", errs.WithCause(errors.Join(os.ErrInvalid, io.EOF)))
}
func main() {
err := generateMultiError()
fmt.Printf("%+v\n", err) // {"Type":"*errs.Error","Err":{"Type":"*errors.errorString","Msg":"error with multiple causes"},"Context":{"function":"main.generateMultiError"},"Cause":{"Type":"*errors.joinError","Msg":"invalid argument\nEOF","Cause":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]}}
fmt.Println(errors.Is(err, io.EOF)) // true
}package main
import (
"errors"
"fmt"
"io"
"os"
"github.com/goark/errs"
)
func generateMultiError() error {
return errs.Join(os.ErrInvalid, io.EOF)
}
func main() {
err := generateMultiError()
fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"invalid argument"},{"Type":"*errors.errorString","Msg":"EOF"}]}
fmt.Println(errors.Is(err, io.EOF)) // true
}package main
import (
"fmt"
"sync"
"github.com/goark/errs"
)
func generateMultiError() error {
errlist := &errs.Errors{}
var wg sync.WaitGroup
for i := 1; i <= 2; i++ {
i := i
wg.Add(1)
go func() {
defer wg.Done()
errlist.Add(fmt.Errorf("error %d", i))
}()
}
wg.Wait()
return errlist.ErrorOrNil()
}
func main() {
err := generateMultiError()
fmt.Printf("%+v\n", err) // {"Type":"*errs.Errors","Errs":[{"Type":"*errors.errorString","Msg":"error 2"},{"Type":"*errors.errorString","Msg":"error 1"}]}
}Use the submodule github.com/goark/errs/zapobject to log errors as structured objects.
logger.Error("failed", zap.Object("error", zapobject.New(err)))Without zapobject, zap.Error(err) writes string fields only.