
Metaerr is a golang package to create or wrap errors with custom metadata and location.
Why
I used github.com/pkg/errors before, and the stack traces were extensive (like Java) and not very useful. Then, I came across the Fault library, which was amazing, but the way I wanted to use it clashed with some of the opinions embedded in the library.
This is why I decided to create this simple library. It utilizes the same "stack trace" model as Fault, in the sense that you will see the stack pertaining to the locations of error creation, and not the stack trace that led to the error's creation.
The next feature it offers is the ability to add any number of key-value metadata entries to each error, including wrapped errors. This is useful if you want to attach metadata at the time of error creation and then leverage that metadata during resolution. A common use case is having a generic HTTP error handler for an API that can use the metadata to determine the HTTP status or construct an error payload to send to the user. Another use case would be logging and alerting. If you convert the metadata into fields in a JSON logger, you could have different alerting rules for logged ERRORS based on the metadata; for example, errors with the metadata tag containing "security" could trigger an immediate alert.
Install
go get -u github.com/quantumcycle/metaerr
Usage
Metaerr can be used with the Go standard errors package, and they are also compatible with error wrapping introduced in Go 1.13.
To create an new MetaErr from a string, use
err := metaerr.New("failure")
To create a new MetaErr by wrapping an existing error, use
err := metaerr.Wrap(err, "failure")
The next step, once you have a Metaerr, is to add metadata to it. You need to create a function that matches the metaerr.ErrorMetadata signature. For your convenience, you can use metaerr.StringMeta, but you can also create your own. Ultimately, all metadata entries are stored as strings.
//Create an metadata called ErrorCode
var ErrorCode = metaerr.StringMeta("error_code")
func main() {
rootCause := metaerr.New("failure")
err := metaerr.Wrap(rootCause, "cannot fetch content").Meta(ErrorCode("x01"))
fmt.Printf("%+v", err)
}
will print (... will be your project location)
cannot fetch content [error_code=x01]
at .../quantumcycle/metaerr/cmd/main.go:13
failure
at .../quantumcycle/metaerr/cmd/main.go:12
In the example above, we use the Printf formatting to display the error, metadata and location all in one gulp. You can however use the provided helper function to get the individual parts
err := metaerr.New("failure")
err.Error() //returns failure
err.Location() //returns .../mysource/mypackage/file.go:22
// will print error_code:x01
meta := metaerr.GetMeta(err, false)
for k, values := range meta {
for _, val := range values {
fmt.Println(k + ":" + val)
}
}
Options
You can provide options to modify the errors during creation. Currently, there is a single option called WithLocationSkip. By default, when creating an error, Metaerr will skip 2 call stack frames to determine the error's creation location. This works well when you call Metaerr directly at the place where the error is created in your codebase. However, there is a use case where you might want to create an error factory function for common scenarios to initialize the error with some standard metadata. In this case, if you use the standard metaerr.New function, the reported location will be the line where metaerr.New is called, which may be within your error factory function. You probably don't want to have all your locations pointing to the same line. To address this, you can use the metaerr.WithLocationSkip option to add additional call stack skips to determine the location. Here is an example:
package main
import (
"fmt"
"github.com/quantumcycle/metaerr"
)
var Tag = metaerr.StringMeta("tag")
func CreateDatabaseError(reason string) error {
return metaerr.New(reason, metaerr.WithLocationSkip(1)).Meta(Tag("database"))
}
func main() {
dbErr := CreateDatabaseError("no such table [User]")
fmt.Printf("%+v", dbErr)
}
which will output
no such table [User] [tag=database]
at /home/matdurand/sources/github/quantumcycle/metaerr/cmd/main.go:16
Without the WithLocationSkip option, the reported location would be line 12, inside the CreateDatabaseError function. Having all our errors pointing to this specific line would ne useless.