Giter Club home page Giter Club logo

fail's Introduction

fail

Build Status codecov GoDoc Go project version Go Report Card License

Better error handling solution especially for application servers.

fail provides contextual metadata to errors.

  • Stack trace
  • Error code (to express HTTP/gRPC status code)
  • Reportability (to integrate with error reporting services)
  • Additional information (tags and params)

Why

Since error type in Golang is just an interface of Error() method, it doesn't have a stack trace at all. And these errors are likely passed from function to function, you cannot be sure where the error occurred in the first place.
Because of this lack of contextual metadata, debugging is a pain in the ass.

Create an error

func New(str string) error

New returns an error that formats as the given text. It also records the stack trace at the point it was called.

func Errorf(format string, args ...interface{}) error

Errorf formats according to a format specifier and returns the string as a value that satisfies error.
It also records the stack trace at the point it was called.

func Wrap(err error, annotators ...Annotator) error

Wrap returns an error annotated with a stack trace from the point it was called, and with the specified options.
It returns nil if err is nil.

Example: Creating a new error

ok := emailRegexp.MatchString("invalid#email.addr")
if !ok {
	return fail.New("invalid email address")
}

Example: Creating from an existing error

_, err := ioutil.ReadAll(r)
if err != nil {
	return fail.Wrap(err)
}

Annotate an error

func WithMessage(msg string) Annotator

WithMessage annotates an error with the message.

func WithMessagef(msg string, args ...interface{}) Annotator

WithMessagef annotates an error with the formatted message.

func WithCode(code interface{}) Annotator

WithCode annotates an error with the code.

func WithIgnorable() Annotator

WithIgnorable annotates an error with the reportability.

func WithTags(tags ...string) Annotator

WithTags annotates an error with tags.

func WithParam(key string, value interface{}) Annotator

WithParam annotates an error with a key-value pair.

// H represents a JSON-like key-value object.
type H map[string]interface{}

func WithParams(h H) Annotator

WithParams annotates an error with key-value pairs.

Example: Adding all contexts

_, err := ioutil.ReadAll(r)
if err != nil {
	return fail.Wrap(
		err,
		fail.WithMessage("read failed"),
		fail.WithCode(http.StatusBadRequest),
		fail.WithIgnorable(),
	)
}

Extract context from an error

func Unwrap(err error) *Error

Unwrap extracts an underlying *fail.Error from an error.
If the given error isn't eligible for retriving context from, it returns nil

// Error is an error that has contextual metadata
type Error struct {
	// Err is the original error (you might call it the root cause)
	Err error
	// Messages is an annotated description of the error
	Messages []string
	// Code is a status code that is desired to be contained in responses, such as HTTP Status code.
	Code interface{}
	// Ignorable represents whether the error should be reported to administrators
	Ignorable bool
	// Tags represents tags of the error which is classified errors.
	Tags []string
	// Params is an annotated parameters of the error.
	Params H
	// StackTrace is a stack trace of the original error
	// from the point where it was created
	StackTrace StackTrace
}

Example

Here's a minimum executable example illustrating how fail works.

package main

import (
	"errors"

	"github.com/k0kubun/pp"
	"github.com/srvc/fail/v4"
)

var myErr = fail.New("this is the root cause")

//-----------------------------------------------
type example1 struct{}

func (e example1) func0() error {
	return errors.New("error from third party")
}
func (e example1) func1() error {
	return fail.Wrap(e.func0())
}
func (e example1) func2() error {
	return fail.Wrap(e.func1(), fail.WithMessage("fucked up!"))
}
func (e example1) func3() error {
	return fail.Wrap(e.func2(), fail.WithCode(500), fail.WithIgnorable())
}

//-----------------------------------------------
type example2 struct{}

func (e example2) func0() error {
	return fail.Wrap(myErr)
}
func (e example2) func1() chan error {
	c := make(chan error)
	go func() {
		c <- fail.Wrap(e.func0(), fail.WithTags("async"))
	}()
	return c
}
func (e example2) func2() error {
	return fail.Wrap(<-e.func1(), fail.WithParam("key", 1))
}
func (e example2) func3() chan error {
	c := make(chan error)
	go func() {
		c <- fail.Wrap(e.func2())
	}()
	return c
}

//-----------------------------------------------
func main() {
	{
		err := (example1{}).func3()
		pp.Println(err)
	}

	{
		err := <-(example2{}).func3()
		pp.Println(err)
	}
}
&fail.Error{
	Err: &errors.errorString{s: "error from third party"},
	Messages: []string{"fucked up!"},
	Code:       500,
	Ignorable:  true,
	Tags:       []string{},
	Params:     fail.H{},
	StackTrace: fail.StackTrace{
		fail.Frame{Func: "example1.func1", File: "stack/main.go", Line: 20},
		fail.Frame{Func: "example1.func2", File: "stack/main.go", Line: 23},
		fail.Frame{Func: "example1.func3", File: "stack/main.go", Line: 26},
		fail.Frame{Func: "main", File: "stack/main.go", Line: 58},
	},
}
&fail.Error{
	Err: &errors.errorString{s: "this is the root cause"},
	Messages:   []string{},
	Code:       nil,
	Ignorable:  false,
	Tags:       []string{"async"},
	Params:     {"key": 1},
	StackTrace: fail.StackTrace{
		fail.Frame{Func: "init", File: "stack/main.go", Line: 10},
		fail.Frame{Func: "example2.func0", File: "stack/main.go", Line: 34},
		fail.Frame{Func: "example2.func1.func1", File: "stack/main.go", Line: 39},
		fail.Frame{Func: "example2.func2", File: "stack/main.go", Line: 44},
		fail.Frame{Func: "example2.func3.func1", File: "stack/main.go", Line: 49},
		fail.Frame{Func: "main", File: "stack/main.go", Line: 64},
	},
}

Example: Server-side error reporting with gin-gonic/gin

Prepare a simple middleware and modify to satisfy your needs:

package middleware

import (
	"net/http"

	"github.com/srvc/fail/v4"
	"github.com/creasty/gin-contrib/readbody"
	"github.com/gin-gonic/gin"

	// Only for example
	"github.com/jinzhu/gorm"
	"github.com/k0kubun/pp"
)

// ReportError handles an error, changes status code based on the error,
// and reports to an external service if necessary
func ReportError(c *gin.Context, err error) {
	failErr := fail.Unwrap(err)
	if failErr == nil {
		// As it's a "raw" error, `StackTrace` field left unset.
		// And it should be always reported
		failErr = &fail.Error{
			Err: err,
		}
	}

	convertFailError(failErr)

	// Send the error to an external service
	if !failErr.Ignorable {
		go uploadFailError(c.Copy(), failErr)
	}

	// Expose an error message in the header
	if msg := failErr.LastMessage(); msg != "" {
		c.Header("X-App-Error", msg)
	}

	// Set status code accordingly
	switch code := failErr.Code.(type) {
	case int:
		c.Status(code)
	default:
		c.Status(http.StatusInternalServerError)
	}
}

func convertFailError(err *fail.Error) {
	// If the error is from ORM and it says "no record found,"
	// override status code to 404
	if err.Err == gorm.ErrRecordNotFound {
		err.Code = http.StatusNotFound
		return
	}
}

func uploadFailError(c *gin.Context, err *fail.Error) {
	// By using readbody, you can retrive an original request body
	// even when c.Request.Body had been read
	body := readbody.Get(c)

	// Just debug
	pp.Println(string(body[:]))
	pp.Println(err)
}

And then you can use like as follows.

r := gin.Default()
r.Use(readbody.Recorder()) // Use github.com/creasty/gin-contrib/readbody

r.GET("/test", func(c *gin.Context) {
	err := doSomethingReallyComplex()
	if err != nil {
		middleware.ReportError(c, err) // Neither `c.AbortWithError` nor `c.Error`
		return
	}

	c.Status(200)
})

r.Run()

fail's People

Contributors

creasty avatar izumin5210 avatar qnighy avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

isgasho

fail's Issues

panic occured when wrap go-playground./validator's errors

from v4.0.0

package main

import (
	"fmt"

	"github.com/srvc/fail"
	validator "gopkg.in/go-playground/validator.v9"
)

func main() {
	err := validator.ValidationErrors{}
	fmt.Println(fail.Wrap(err))
}
panic: runtime error: comparing uncomparable type validator.ValidationErrors

goroutine 1 [running]:
github.com/srvc/fail.extractPkgError(0x1be6a0, 0x40cfe0, 0x450230, 0x277c00)
	/go/src/github.com/srvc/fail/pkgerrors.go:82 +0x960
github.com/srvc/fail.convertPkgError(0x1be6a0, 0x40cfe0, 0x1901c0, 0x9f442304)
	/go/src/github.com/srvc/fail/pkgerrors.go:22 +0x40
github.com/srvc/fail.Unwrap(0x1be6a0, 0x40cfe0, 0x15e901, 0x7622)
	/go/src/github.com/srvc/fail/error.go:113 +0x260
github.com/srvc/fail.Wrap(0x1be6a0, 0x40cfe0, 0x0, 0x0, 0x0, 0x4423a0, 0x0, 0x0)
	/go/src/github.com/srvc/fail/error.go:85 +0x40
main.main()
	/go/src/29f35e98-5c00-4964-a695-ff39d667e9e2/main.go:12 +0xa0

New(), Errorf() with Annotators

Now, annotators cannot be added when generating new error.
If we want to generate new error with annotations, we have to do like following.

fail.Wrap(fail.New("hoge"), fail.WithCode(400))

So, I want you to be able to attach annotations with fail.New() and fail.Errorf(), or create another methods.

func New(text string, annotators ...Annotator) error {
	err := &Error{Err: errors.New(text)}
	withStackTrace(0)(err)
	for _, f := range annotators {
		f(err)
	}
	return err
}

func Errorf(format string, args ...interface{}) error {
	var fmtArgs []interface{}
	var annotators []Annotator
	for _, arg := range args {
		antt, ok := arg.(Annotator)
		if ok {
			annotators = append(annotators, antt)
		} else {
			fmtArgs = append(fmtArgs, arg)
		}
	}
	err := &Error{Err: fmt.Errorf(format, fmtArgs...)}
	withStackTrace(0)(err)
	for _, f := range annotators {
		f(err)
	}
	return err
}

Might want to change i/f of annotators

What

  • Change prefix from With to Set
    • e.g., WithMessage โ†’ SetMessage
    • For example, WithIgnorable doesn't sound natural whereas SetIgnorable makes sense
  • Once you annotate errors with WithIgnorable, you can't undo it
    • How about changing it to accept a boolean as a parameter?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.