Categorygithub.com/creasty/apperrors
modulepackage
1.0.0
Repository: https://github.com/creasty/apperrors.git
Documentation: pkg.go.dev

# README

apperrors

Build Status codecov GoDoc License

Better error handling solution especially for application server.

apperrors provides contextual metadata to errors.

  • Stack trace
  • Additional information
  • Status code (for a HTTP server)
  • Reportability (for an integration with error reporting service)

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.

How different from pkg/errors

:memo: apperrors supports pkg/errors. It reuses pkg/errors's stack trace data of the innermost (root) error, and converts into apperrors's data type.

TBA

Create an error

func New(str string) error

New returns an error that formats as the given text.
It also annotates the error with a stack trace from 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 annotates the error with a stack trace from the point it was called

func Wrap(err error) error

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

Example: Creating a new error

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

Example: Creating from an existing error

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

Annotate an error

func WithMessage(err error, msg string) error

WithMessage wraps the error and annotates with the message.
If err is nil, it returns nil

func WithStatusCode(err error, code int) error

WithStatusCode wraps the error and annotates with the status code.
If err is nil, it returns nil

func WithReport(err error) error

WithReport wraps the error and annotates with the reportability.
If err is nil, it returns nil

Example: Adding all contexts

_, err := ioutil.ReadAll(r)
if err != nil {
	return apperrors.WithReport(
		apperrors.WithStatusCode(
			apperrors.WithMessage(err, "read failed"),
			http.StatusBadRequest
		)
	)
}

Extract context from an error

func Unwrap(err error) *Error

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

type Error struct {
	// Err is the original error (you might call it the root cause)
	Err error
	// Message is an annotated description of the error
	Message string
	// StatusCode is a status code that is desired to be used for a HTTP response
	StatusCode int
	// Report represents whether the error should be reported to administrators
	Report bool
	// 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 describing how apperrors works.

package main

import (
	"errors"

	"github.com/creasty/apperrors"
	"github.com/k0kubun/pp"
)

func errFunc0() error {
	return errors.New("this is the root cause")
}
func errFunc1() error {
	return apperrors.Wrap(errFunc0())
}
func errFunc2() error {
	return apperrors.WithMessage(errFunc1(), "fucked up!")
}
func errFunc3() error {
	return apperrors.WithReport(apperrors.WithStatusCode(errFunc2(), 500))
}

func main() {
	err := errFunc3()
	pp.Println(err)
}
$ go run main.go
&apperrors.Error{
  Err:        &errors.errorString{s: "this is the root cause"},
  Message:    "fucked up!",
  StatusCode: 500,
  Report:     true,
  StackTrace: apperrors.StackTrace{
    apperrors.Frame{Func: "errFunc1", File: "main.go", Line: 13},
    apperrors.Frame{Func: "errFunc2", File: "main.go", Line: 16},
    apperrors.Frame{Func: "errFunc3", File: "main.go", Line: 19},
    apperrors.Frame{Func: "main", File: "main.go", Line: 23},
    apperrors.Frame{Func: "main", File: "runtime/proc.go", Line: 194},
    apperrors.Frame{Func: "goexit", File: "runtime/asm_amd64.s", Line: 2198},
  },
}

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/creasty/apperrors"
	"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) {
	appErr := apperrors.Unwrap(err)
	if appErr == nil {
		// As it's a "raw" error, `StackTrace` field left unset.
		// And it should be always reported
		appErr = &apperrors.Error{
			Err:    err,
			Report: true,
		}
	}

	convertAppError(appErr)

	// Send the error to an external service
	if appErr.Report {
		go uploadAppError(c.Copy(), appErr)
	}

	// Expose an error message in the header
	if appErr.Message != "" {
		c.Header("X-App-Error", appErr.Message)
	}

	// Set status code accordingly
	if appErr.StatusCode > 0 {
		c.Status(appErr.StatusCode)
	} else {
		c.Status(http.StatusInternalServerError)
	}
}

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

func uploadAppError(c *gin.Context, err *apperrors.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()

# Functions

Errorf formats according to a format specifier and returns the string as a value that satisfies error.
New returns an error that formats as the given text.
Unwrap extracts an underlying *apperrors.Error from an error.
WithMessage wraps the error and annotates with the message.
WithReport wraps the error and annotates with the reportability.
WithStatusCode wraps the error and annotates with the status code.
Wrap returns an error annotated with a stack trace from the point it was called.

# Structs

Error is an error that has contextual metadata.
Frame represents a single frame of stack trace.

# Type aliases

StackTrace is a stack of Frame from innermost to outermost.