e

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 18, 2020 License: MIT Imports: 5 Imported by: 0

README

e

Go

pkg.go.dev link

Inspired by Failure is your Domain and Error handling in upspin (comparison here), package e is designed to meet the needs of web applications by maintaining a clean separation between the error data consumed by end-users, clients, and operators.

e focuses on three principles:

  • Simplicity: Small, fluent interface for developer ergonomics

  • Safety: Explicitly separate end-user messages from internal errors

  • Compatibility: Follows error-wrapping conventions and can be adopted incrementally

Creating a new Error

e.NewError() populates the inner error message with the calling function name at runtime.

const CodeInvalidError = "invalid_error"

func Foo(bar string) error {
    if bar == nil {
        return e.NewError(CodeInvalidError, "bar cannot be nil")
        // "Foo: [invalid_error] bar cannot be nil"
    }
    return nil
}

e.NewErrorf() allows for formatted strings.

Wrapping an existing Error

e.Wrap() also automatically injects calling function name, reducing the burden on developers of providing context to the error and helping keep the error stack free of redundant or unhelpful messages.

In most cases it is enough to simply e.Wrap(err) .

func Foo(bar string) error {
    err := db.GetBar(bar) // "GetBar: [database_error] cannot find bar"
    if err != nil {
        return e.Wrap(err)
        // "Foo: GetBar: [database_error] cannot find bar"
    }
    return nil
}

Wrap() can take an optionalInfo param to inject additional context into the error stack.

func Foo(bar string) error {
    err := db.GetBar(bar) // "GetBar: [database_error] cannot find bar"
    if err != nil {
        return e.Wrap(err, fmt.Sprintf("bar id: %s", bar.Id))
        // "Foo: (bar id: 2hs8qh9): GetBar: [database_error] cannot find bar"
    }
    return nil
}

Wrapf() allows for formatted strings.

Wrapping a different error type

e.Wrap() can be chained with SetCode() to provide a new code for errors from another package (or default go errors).

// db.GetBar returns sentinel error:
//     var ErrNotFound = errors.New("not found")

const CodeInternalError = "internal_error"

func Foo(bar string) error {
    err := db.GetBar(bar) // "not found"
    if err != nil {
        return e.Wrap(err).SetCode(CodeInternalError)
        // "Foo: [internal_error] not found
    }
}

Handling Errors

End-user

ErrorMessage() is used to display a user-friendly error message to the end-user. NewError() and Wrap() do not have a message param (intentional design). SetMessage() should be called to assign an intentional and meaningful message.

func doSomething(r *http.Request ctx context.Context) error {
    err := Foo()
    if err != nil {
        return e.Wrap(err).SetMessage("Oh no! An Error has occurred.")
        // this message will not show in Error() and can only be retrieved
        // by e.Message() or ErrorMessage()
    }
    return nil
}

This behaviour should be combined with a default error message at the handler level to make message an optional field that should only be set to give additional context to the end-user.

func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    bytes, err := doSomething(r)
    if err != nil {
        // Log full error stack
        logger.Error(err)

        // Extract the first error message from the stack
        userMsg := e.ErrorMessage(err)
        if userMsg == "" {
            userMsg = "Unexpected error has occurred"
        }

        http.Error(w, userMsg, http.StatusInternalServerError)
        return
    }
    w.Write(bytes)
}
Client

ErrorCode() is intended for use by any clients such as front-end applications, other libraries, and even callers within your own application. In the context of a go codebase, code provides an alternative way of introspecting error types without comparing Error() strings or using type assertions.

const (
    CodeInvalidError = "invalid_error"
    CodeDatabaseError = "database_error"
)

func Foo(bar string) error {
    if bar == nil {
        return e.NewError(CodeInvalidError, "bar cannot be nil")
    }
    err := db.GetBar(bar)
    if err != nil {
        return e.Wrap(err).SetCode(CodeDatabaseError)
        // SetCode() may not be necessary if we control the err
        // returned from db.GetBar().
    }
    return nil
}

func doSomething(r *http.Request) error {
    err := Foo(r.FormValue("bar"))
    if err != nil {
        var info string
        switch e.ErrorCode(err) {
        case CodeInvalidError:
            info = "Invalid Request"
        case CodeDatabaseError:
            err2 := RetryFoo(r.FormValue("bar"))
            if err2 != nil {
                info = "Database error has occurred. Please try again."
            } else {
                return nil
            }
        default:
            info = "Unexpected error has occurred. Please try again"
        }
        return e.Wrap(err).SetMessage(info)
    }
    return nil
}
Operator

Operators are usually the developers or application support who are concerned with logging the logical stack trace of the error. By automatically injecting the calling function name when we e.NewError() or e.Wrap(), we can easily build a chain of functions that were called down the stack to the error site.

Logging Error() is sufficient to write the stack in this format:

op: (additionalInfo): [code] cause

Examples:

Foo: cannot find bar

DoSomething: Foo: [database_error] cannot find bar

ServeHTTP: DoSomething: Foo: (cannot find bar 2hjk7d): GetBar: getBarById: [database_error] cannot find bar

// anonymous functions
Foo.func1: cannot find bar

// goroutines
Foo.func1.1: cannot find bar

In addition, both e.NewError() and e.Wrap() populate a more detailed stacktrace that can be retrieved with ErrorStack(err) (also compatible with any error type that fulfils HasStacktrace):

err := handleSomething()
if err != nil {
    logger.Error({
        error: err, 
        stack: ErrorStack(err), // may be ""
    })
}

Comparisons with other approaches

Upspin

Package e leverages the const op = "FuncName" pattern introduced by Rob Pike and Andrew Gerrard in Upspin to build a logical stacktrace but uses runtime libraries to automatically extract the function name.

In place of Upspin's multi-purpose E(args ...interface{}) function, e uses the familiar verbs New and Wrap to provide better type safety and simpler implementation.

Upspin did not have a clear separation between messages for end-users and the error stack, making it unsuitable for a web application which needs to hide internal details.

Ben Johnson's Failure is your Domain

Package e is heavily influenced by Ben's approach to error-handling outlined in his blog post Failure is your Domain. These are some key differences:

  • e does not rely on struct initialization to create errors and instead uses NewError(), Wrap() and other helper methods to guarantee valid internal state

  • e does not require the error stack to have uniform type, and is compatible with other error types

  • e keeps message logically distinct from the error stack, making it suitable for displaying to an end-user

Documentation

Overview

Package e is an error-handling package designed to be simple, safe, and compatible.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ErrorCode

func ErrorCode(err error) string

ErrorCode returns the first unwrapped Code of an error which implements ClientFacing interface. Otherwise returns an empty string.

func ErrorMessage

func ErrorMessage(err error) string

ErrorMessage returns the first unwrapped Message of an error which implements ClientFacing interface. Otherwise returns an empty string.

func ErrorStacktrace added in v1.0.0

func ErrorStacktrace(err error) string

ErrorStacktrace returns the innermost Stack of an error which implements HasStacktrace interface. Otherwise returns an empty string.

Types

type ClientFacing

type ClientFacing interface {

	// ClientCode returns the a short string representing the type of error, such as
	// "database_error", to be used by a client or an application.
	//
	// Note: ErrorCode() should be used to retrieve the topmost Code()
	ClientCode() string

	// ClientMessage returns a user-friendly error message (if any) which is logically
	// separate from the error cause.
	//
	// Note: ErrorMessage() should be used to retrieve the topmost Message().
	ClientMessage() string
}

ClientFacing allows custom error types to be used with utility functions ErrorCode() and ErrorMessage().

type Error

type Error interface {
	error
	ClientFacing
	HasStacktrace

	Unwrap() error

	// SetCode adds an error type to a non-nil Error such as "unexpected_error",
	// "database_error", "not_exists", etc.
	//
	// Will panic when used with a nil Error receiver.
	SetCode(code string) Error

	// SetMessage adds a user-friendly message to a non-nil Error.
	// Message will not be printed with Error() and should be retrieved with ErrorMessage().
	//
	// Will panic when used with a nil Error receiver.
	SetMessage(message string) Error
}

Error represents a standard application error. Implements ClientFacing and HasStacktrace so it can be introspected with functions like ErrorCode, ErrorMessage, and ErrorStacktrace.

func NewError added in v1.0.0

func NewError(code, cause string) Error

NewError constructs a new Error. code should be a short, single string describing the type of error (typically a pre-defined const). cause is used to create the nested error which will act as the root of the error stack.

Usage:

func Foo(bar *Bar) error {
	if bar == nil {
		return e.New("unexpected_error", "bar is nil")
	}
	return doFoo(bar)
}

func NewErrorf added in v1.0.0

func NewErrorf(code, fmtCause string, args ...interface{}) Error

NewErrorf constructs a new Error with formatted string. code should be a short, single string describing the type of error (typically a pre-defined const). cause is used to create the nested error which will act as the root of the error stack.

Usage:

func Foo(bar Bar) error {
	done := doFoo(bar)
	if !done {
		return e.NewErrorf("unexpected_error", "cannot process bar: %v", bar)
	}
	return nil
}

func Wrap

func Wrap(err error, optionalInfo ...string) Error

Wrap adds the name of the calling function to the wrapped error. OptionalInfo can be passed to insert more context at the wrap site. Only the first OptionalInfo string will be used.

Basic usage:

err := Foo()
if err != nil {
	return e.Wrap(err)
}

Adding additional info:

err := Foo()
if err != nil {
	return e.Wrap(err, fmt.Sprintf("cannot find id: %v", id))
}

func Wrapf added in v1.0.0

func Wrapf(err error, fmtInfo string, args ...interface{}) Error

Wrapf adds the name of the calling function and a formatted message to the wrapped error.

Basic usage:

err := Foo(bar)
if err != nil {
	return e.Wrapf(err, "bar not found: %v", bar.Id)
}

type HasStacktrace added in v1.0.0

type HasStacktrace interface {

	// Stacktrace returns the innermost stacktrace, if any.
	Stacktrace() string
}

HasStacktrace allows custom error types to be used with utility function ErrorStacktrace().

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL