Oh Fudge! A straight-forward error library for Go.
- Implements idiomatic standard library functions
- Unwrap wrapped errors using
errors.Unwrap
- Compare errors with
errors.Is
- Unwrap wrapped errors using
- Contextual messages
- Structured key value pairs
- Stack traces
- Custom formatting
- gRPC support
- Multi error support (planned)
Install using the following
go get github.com/rossmacarthur/fudge
And import this package instead of the standard library errors
import "github.com/rossmacarthur/fudge/errors"
Construct errors in the usual way
errors.New("razor not found")
Or wrap a standard library or Fudge sentinel
errors.Wrap(io.EOF, "failed to shave yak")
The error will be constructed with a stack trace attached. Now the error can be passed up the call stack like normal.
if err != nil {
return err
}
Optionally, existing errors can be wrapped with additional context and key value pairs.
if err != nil {
return errors.Wrap(err, "failed to shave yak", fudge.KV("yak_id", yakID))
}
Formatting this error with %+v
results in the following
fmt.Printf("%+v\n", err)
failed to shave yak: razor not found
example/razor.go:20 locateRazor
example/razor.go:26 example
example/main.go:13 main
runtime/proc.go:250 main
runtime/asm_arm64.s:1172 goexit
Error construction is straight-forward. Fudge considers messages formatted with
contextual information to be an antipattern so no Newf
function is provided.
Instead key value pairs should be used.
-
Ordinary inline errors
errors.New("failed to shave yak")
-
Inline errors with a key value pair
errors.New("failed to shave yak", fudge.KV("yak_id", yakID))
-
Inline errors with multiple key value pairs
errors.New("failed to shave yak", fudge.MKV{"yak_id": yakID, "hair_len": hairLen})
-
Wrap an existing Fudge sentinel, this adds a stack trace
errors.Wrap(ErrRazorNotFound, "failed to shave yak")
-
Wrap an existing non-Fudge error, this wraps it with a Fudge error
_, err := os.ReadFile(path) if err != nil { return errors.Wrap(err, "failed to read file") }
-
Wrap an existing non-Fudge sentinel, this wraps it with a Fudge error
errors.Wrap(io.EOF, "failed to read file")
Sentinel errors are typically defined in the global scope and do not get a stack
trace attached until they are wrapped with Wrap
. A code is required in order
for the error to be passed across gRPC in such a way that errors.Is
checks
still work. If you don't need this behaviour then you can just define sentinels
using errors.New
.
// If you need gRPC support
var ErrShavingFailed = errors.Sentinel("failed to shave yak", "ERR_0a8cba3dfa944ecb")
// Otherwise this is fine
var ErrShavingFailed = errors.New("failed to shave yak")
💡 The fudge
command can automatically generate sentinel error
codes for you.
Any error can be compared against sentinels using errors.Is
no matter how
many times they were wrapped.
var ErrRazorNotFound = errors.New("razor not found")
if errors.Is(err, io.EOF) {
// ...
} else if errors.Is(err, ErrRazorNotFound) {
// ...
}
However, this doesn't work when errors are passed over the wire using gRPC. In
order for that to work the sentinel error must be defined using
errors.Sentinel
which assigns a unique code. This code is then passed over the
wire.
var ErrRazorNotFound = errors.Sentinel("razor not found", "ERR_0a8cba3dfa944ecb")
For example given the following.
var ErrRazorNotFound = errors.New("razor not found")
func locateRazor() error {
return errors.Wrap(ErrRazorNotFound, "", fudge.KV("hair_len", hairLen))
}
err := locateRazor()
if err != nil {
return errors.Wrap(err, "failed to shave yak", fudge.KV("yak_id", yakID))
}
By default .Error()
output will return a colon (:
) separated list of
contextual messages like this:
failed to shave yak: razor not found
If formatted using fmt.Sprintf("%+v", err)
the stack trace will be shown with
any added contextual messages.
failed to shave yak: razor not found
example/main.go:20 locateRazor
example/main.go:24 example
example/main.go:26 example
example/main.go:13 main
runtime/proc.go:250 main
runtime/asm_arm64.s:1172 goexit
If formatted using fmt.Sprintf("%#v", err)
then the key value pairs will also
be added.
failed to shave yak: razor not found {hair_len:7, yak_id:1337}
example/main.go:20 locateRazor
example/main.go:24 example
example/main.go:26 example
example/main.go:13 main
runtime/proc.go:250 main
runtime/asm_arm64.s:1172 goexit
Custom formatting is possible. For example:
ferr := new(errors.Error)
if ok := errors.As(err, &ferr); ok {
fmt.Printf("Code:\n %s\nTraceback:\n", ferr.Code)
for _, frame := range ferr.Trace {
fmt.Printf("%s:%d\n", frame.File, frame.Line)
}
}
The errors/grpc
package provides gRPC interceptors that can be used to
serialize and deserialize errors over the wire.
On the server add the following interceptors.
import (
errorsgrpc "github.com/rossmacarthur/fudge/errors/grpc"
)
grpc.NewServer(
grpc.UnaryInterceptor(errorsgrpc.UnaryServerInterceptor),
grpc.StreamInterceptor(errorsgrpc.StreamServerInterceptor))
On the client add the following interceptors.
import (
errorsgrpc "github.com/rossmacarthur/fudge/errors/grpc"
)
grpc.DialContext(ctx, addr,
grpc.WithUnaryInterceptor(errorsgrpc.UnaryClientInterceptor),
grpc.WithStreamInterceptor(errorsgrpc.StreamClientInterceptor))
The fudge
command is provided to automatically generate error codes for
sentinel errors.
go install github.com/rossmacarthur/fudge/cmd/fudge
The command recursively rewrites Go files in a directory to add codes. E.g. given the following code.
var ErrShavingFailed = errors.Sentinel("failed to shave yak", "")
It would be automatically updated to something like this.
var ErrShavingFailed = errors.Sentinel("failed to shave yak", "ERR_0a8cba3dfa944ecb")
Inspired by github.com/luno/jettison.
This project is distributed under the terms of both the MIT license and the Apache License (Version 2.0).
See LICENSE-APACHE and LICENSE-MIT for details.