Table of Content
Errors are a key element of Golang. By understanding errors and handling them properly, you can create robust applications with the context needed to troubleshoot failures. This article will guide you through our journey creating the errwrap package, which contains a factory for standardizing error messages and improving context information using the Golang error wrapping mechanism.
Handling errors in Golang
Golang does not provide exceptions or the conventional try/catch mechanism to handle errors. Instead, the language treats errors as values by exposing a built-in error interface to abstract this concept.
type error interface {
Error() string
}
That means any type that implements an Error() string
function can be used to represent an error condition. By convention the errors are the last return value of the functions.
type ParseError struct {
Value interface{}
DestinationType string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("failed to parse '%v' as %s",
e.Value,
e.DestinationType)
}
func ParseBool(str string) (bool, error) {
switch str {
case "True", "true", "T", "t", "1":
return true, nil
case "False", "false", "F", "f", "0":
return false, nil
default:
return false, &ParseError{Value: str, DestinationType: "bool"}
}
}
However, instead of creating custom error types, most of the time it is enough to use the built-in errors.New() and fmt.Errorf() functions provided by the language standard library.
return false, errors.New("failed to parse value as bool")
return false, fmt.Errorf("failed to parse '%s' as bool", str)
Anyone who calls the ParseBool()
function will need to make sure the error is not nil
, keeping the happy path as left indented as possible. The most common action for errors is to annotate them with context information and escalate them until they reach a level in the function call hierarchy where they can be handled properly. For example by logging it, terminating the application with a non-zero exit code, or returning a HTTP response with a 4xx or 5xx status code.
Annotating errors with context information
Before Go 1.13 was released, the way to add context to errors using the standard library was to create a new error, with a message that includes the context information and the previous error message.
dryRun, err := ParseBool(value)
if err != nil {
return fmt.Errorf("invalid value for --dry-run flag: %v", err)
}
If we call the previous function with a value that cannot be parsed as a boolean, for example foo
, then we get an error that says invalid value for --dry-run flag: failed to parse 'foo' as bool
.
The main downside to wrapping errors this way is that the original error information is lost. If we need to extract specific information about the error, or if we need to check for a specific error type, we can’t do it after it is wrapped.
func main() {
args, err := ParseArguments(os.Args\[1:\])
if err != nil {
// This type assertion is not possible, because after
// wrapping the error its type is no longer *ParseError.
perr, ok := err.(*ParseError)
if ok {
// do something with perr
}
log.Fatalf("Failed to parse command line arguments: %v", err)
}
}
This issue can be fixed by using the package https://github.com/pkg/errors created by Dave Cheney. It introduces a new way to wrap errors while also being able to retrieve the original error.
dryRun, err := ParseBool(value)
if err != nil {
return errors.Wrap(err, "invalid value for --dry-run flag")
}
func main() {
args, err := ParseArguments(os.Args\[1:\])
if err != nil {
perr, ok := errors.Cause(err).(*ParseError)
if ok {
// do something with perr
}
log.Fatalf("Failed to parse command line arguments: %v", err)
}
}
This package was widely used by the community until Go 1.13 introduced a built-in error wrapping mechanism to the standard library. It adds support to a new verb %w in the existing fmt.Errorf()
function to create wrapped errors. It also adds three new functions to the errors package to inspect the wrapped error (errors.Unwrap, errors.Is, errors.As
) .
dryRun, err := ParseBool(value)
if err != nil {
return fmt.Errorf("invalid value for --dry-run flag: %w", err)
}
func main() {
args, err := ParseArguments(os.Args\[1:\])
if err != nil {
var perr *ParseError
if errors.As(err, &perr) {
// do something with perr
}
log.Fatalf("Failed to parse command line arguments: %v", err)
}
}
Implementing the error factory
When Go 1.13 was released, LTVCo updated our applications that were using Github pkg errors to start using the new functions in the “errors” package of the standard library. When doing this we noticed that much of the error handling logic in our codebase was duplicated and not standardized. We saw this as an opportunity to extract that logic to a common place, and that was how our “errwrap” package was born.
One of the standards we have is that error messages must be prepended with the name of the package that creates or wraps the error. We considered this when designing the errwrap package and created a factory object. The goal is that every package has its own error factory configured with the package name, and uses that factory for all error handling performed in the package.
// Factory creates and wraps errors.
type Factory struct {
err error
prefix string
wrapping bool
}
// NewFactory creates a new error factory with a given prefix.
// The prefix will be prepended to all errors returned by the factory.
func NewFactory(prefix string) *Factory {
return &Factory{prefix: fmt.Sprintf("%s: ", prefix)}
}
The first two methods exposed by the factory are Error()
and Errorf()
, which can be used to create errors using a raw error message or a format specifier.
// Error behaves like errors.New() but prepends the factory prefix to the error message.
func (f *Factory) Error(text string) error {
return errors.New(f.prefix + text)
}
// Errorf behaves like fmt.Errorf() but prepends the factory prefix to the error message.
func (f *Factory) Errorf(format string, a ...interface{}) error {
return fmt.Errorf(f.prefix+format, a...)
}
When we started using the error factory we noticed that the error messages were not standardized. Some of them started with “failed to …” and some others started with “unable to …”, so we decided to use only “failed to …” to standardize the error messages. We added two helpers to the factory:
// FailedTo returns an error with a message "failed to <action>".
func (f *Factory) FailedTo(action string) error {
return f.Errorf("failed to %s", action)
}
// FailedTof behaves like Errorf but prepends "failed to".
func (f *Factory) FailedTof(format string, a ...interface{}) error {
return f.Errorf("failed to "+format, a...)
}
We also noticed some common validation errors and added helpers for them too.
// Nil returns an error with a message "nil <something>".
func (f *Factory) Nil(something string) error {
return f.Errorf("nil %s", something)
}
// Empty returns an error with a message "empty <something>".
func (f *Factory) Empty(something string) error {
return f.Errorf("empty %s", something)
}
At this point the factory has the methods and helpers for creating errors but is missing one important feature: being able to wrap them. To do this we used the builder pattern by introducing a Wrap()
method that receives an error and returns a derived factory which wraps the given error in all future errors created.
// Wrap returns a derived Factory that wraps the given errors in all errors created.
func (f *Factory) Wrap(err error) *Factory {
return &Factory{
err: err,
prefix: f.prefix,
wrapping: true,
}
}
// Error behaves like errors.New() but prepends the factory prefix to the error message.
func (f *Factory) Error(text string) error {
return f.wrap(errors.New(f.prefix + text))
}
// Errorf behaves like fmt.Errorf() but prepends the factory prefix to the error message.
func (f *Factory) Errorf(format string, a ...interface{}) error {
return f.wrap(fmt.Errorf(f.prefix+format, a...))
}
func (f *Factory) wrap(err error) error {
if !f.wrapping {
return err
}
if f.err == nil {
return nil
}
return &wrapError{inner: f.err, outer: err}
}
The wrapError
is an error type which contains the error being wrapped and the context information used to wrap the error:
package errwrap
import "fmt"
type wrapError struct {
inner error
outer error
}
func (w *wrapError) Error() string {
return fmt.Sprintf("%v: %v", w.outer, w.inner)
}
func (w *wrapError) Unwrap() error {
return w.inner
}
Now the error factory is complete and ready to be used.
package main
var errors = errwrap.NewFactory("main")
func main() {
if err := run("foo", "bar"); err != nil {
log.Println(err)
return
}
log.Println("Done")
}
func run(dryRun string, verbose string) error {
_, err := strconv.ParseBool(dryRun)
if err != nil {
return errors.Wrap(err).FailedTo("parse --dry-run flag")
}
_, err = strconv.ParseBool(verbose)
if err != nil {
return errors.Wrap(err).FailedTo("parse --verbose flag")
}
// execute application logic
return nil
}
// output:
// main: failed to parse --dry-run flag: strconv.ParseBool: parsing "foo": invalid syntax
By using error wrapping mechanism we were able to identify which of the flags failed to be parsed. All the code and examples used in this article are available in the following Go Playground. The errwrap package helped us to standardize our error messages between applications and improve the context information in them. In a future blog post we will discuss how to adapt this error factory to wrap multiple errors, and the use cases of that feature.
Top comments (0)