DEV Community

Cover image for Implementing an Error Factory in Go
LTV Co. Engineering
LTV Co. Engineering

Posted on • Originally published at ltvco.com

Implementing an Error Factory in Go

#go

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

}  
Enter fullscreen mode Exit fullscreen mode

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"}

    }

}  
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode
return false, fmt.Errorf("failed to parse '%s' as bool", str)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}  
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode
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)

    }

}  
Enter fullscreen mode Exit fullscreen mode

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)

}  
Enter fullscreen mode Exit fullscreen mode
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)

    }

}  
Enter fullscreen mode Exit fullscreen mode

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)}

}  
Enter fullscreen mode Exit fullscreen mode

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...)

}  
Enter fullscreen mode Exit fullscreen mode

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...)

}  
Enter fullscreen mode Exit fullscreen mode

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)

}  
Enter fullscreen mode Exit fullscreen mode

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}

}  
Enter fullscreen mode Exit fullscreen mode

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

}  
Enter fullscreen mode Exit fullscreen mode

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  
Enter fullscreen mode Exit fullscreen mode

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.


Image description

Top comments (0)