An error handling package to add additional structured fields to errors. This package helps you keep the only handle errors once rule while not losing context where the error occurred.
includes a stack trace so logging can report the exact location where the error occurred.
Includes Wrapf()
and Wrap()
variants
return errors.Wrapf(err, "while reading '%s'", fileName)
Identical to errors.Wrap()
but you don't need a message, just a stack trace to where the error occurred.
return errors.Stack(err)
Attach additional fields to the error and a stack trace to give structured logging as much context
to the error as possible. Includes Wrap()
, Wrapf()
, Stack()
, Error()
and Errorf()
variants
return errors.Fields{"fileName": fileName}.Wrapf(err, "while reading '%s'", fileName)
return errors.Fields{"fileName": fileName}.Stack(err)
return errors.Fields{"fileName": fileName}.Error("while reading")
Works just like errors.Fields{}
but allows collecting and passing around fields independent of the point of error
creation. In functions with many exit points this can result in cleaner less cluttered looking code.
fields := map[string]any{
"domain.id": domainId,
}
err, accountID := account.GetByDomain(domainID)
if err != nil {
// Error only includes `domain.id`
return errors.WrapFields(err, fields, "during call to account.GetByDomain()")
}
fields["account.id"] = accountID
err, disabled := domain.Disable(accountID, domainID)
if err != nil {
// Error now includes `account.id` and `domain.id`
return errors.WrapFields(err, fields, "during call to domain.Disable()")
}
Works just like errors.As()
except it returns the last error in the chain instead of the first. In
this way you can discover the target which is closest to where the error occurred.
// Returns the last error in the chain that has a stack trace attached
var last callstack.HasStackTrace
if errors.Last(err, &last)) {
fmt.Printf("Error occurred here: %+v", last.StackTrace())
}
A convenience function to extract all stack and field information from the error.
err := io.EOF
err = errors.WithFields{"fileName": "file.txt"}.Wrap(err, "while reading")
m := errors.ToMap(err)
fmt.Printf("%#v\n", m)
// OUTPUT
// map[string]interface {}{
// "excFileName":"/path/to/wrap_test.go",
// "excFuncName":"my_package.ReadAFile",
// "excLineNum":42,
// "excType":"*errors.errorString",
// "excValue":"while reading: EOF",
// "fileName":"file.txt"
// }
A convenience function to extract all stack and field information from the error in a form appropriate for logrus.
err := io.EOF
err = errors.WithFields{"fileName": "file.txt"}.Wrap(err, "while reading")
f := errors.ToLogrus(err)
logrus.WithFields(f).Info("test logrus fields")
// OUTPUT
// time="2023-02-20T19:11:05-06:00"
// level=info
// msg="test logrus fields"
// excFileName=/path/to/wrap_test.go
// excFuncName=my_package.ReadAFile
// excLineNum=21
// excType="*errors.wrappedError"
// excValue="while reading: EOF"
Provides pass through access to the standard errors.Is()
, errors.As()
, errors.Unwrap()
so you don't need to
import this package and the standard error package.
If you are working at mailgun and are using scaffold; using logrus.WithError(err)
will cause logrus to
automatically retrieve the fields attached to the error and index them into our logging system as separate
searchable fields.
If you have custom http middleware for handling unhandled errors, this is an excellent way to easily pass additional information about the request up to the error handling middleware.
Errors wrapped with errors.WithFields{}
are compatible with standard library introspection functions errors.Unwrap()
,
errors.Is()
and errors.As()
ErrQuery := errors.New("query error")
wrap := errors.WithFields{"key1": "value1"}.Wrap(err, "message")
errors.Is(wrap, ErrQuery) // == true
The fields wrapped by errors.WithFields{}
are not intended to be used to by code to decide how an error should be
handled. It is intended as a convenience where the failure is well known, but the context is dynamic. In other words,
you know the database returned an unrecoverable query error, but you want to attach localized context information
to the error.
As an example
func (r *Repository) FetchAuthor(customerID, isbn string) (Author, error) {
// Returns ErrorNotFound{} if not exist
book, err := r.fetchBook(isbn)
if err != nil {
return nil, errors.WithFields{"customer.id": customerID, "isbn": isbn}.Wrap(err, "while fetching book")
}
// Returns ErrorNotFound{} if not exist
author, err := r.fetchAuthorByBook(book)
if err != nil {
return nil, errors.WithFields{"customer.id" customerID, "book": book}.Wrap(err, "while fetching author")
}
return author, nil
}
Now you can easily search your structured logs for errors related to customer.id
.
You should continue to create and inspect custom error types
type ErrAuthorNotFound struct {
Msg string
}
func (e *ErrAuthorNotFound) Error() string {
return e.Msg
}
func (e *ErrAuthorNotFound) Is(target error) bool {
_, ok := target.(*NotFoundError)
return ok
}
func main() {
r := Repository{}
author, err := r.FetchAuthor("isbn-213f-23422f52356")
if err != nil {
// Fetch the original and determine if the error is recoverable
if error.Is(err, &ErrAuthorNotFound{}) {
author, err := r.AddBook("isbn-213f-23422f52356", "charles", "darwin")
}
if err != nil {
logrus.WithFields(errors.ToLogrus(err)).
WithError(err).Error("while fetching author")
os.Exit(1)
}
}
fmt.Printf("Author %+v\n", author)
}