├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── annotators.go ├── error.go ├── error_go113_test.go ├── error_test.go ├── go.mod ├── go.sum ├── hash.go ├── hash_test.go ├── pkgerrors.go ├── pkgerrors_test.go ├── stack.go └── stack_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | coverage.txt 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | matrix: 4 | include: 5 | - go: '1.11.x' 6 | - go: '1.12.x' 7 | - go: '1.13.x' 8 | 9 | branches: 10 | only: 11 | - master 12 | 13 | env: 14 | global: 15 | - GO111MODULE=on 16 | 17 | # CODECOV_TOKEN 18 | - secure: "0eWuB+duerrzTHke7+4RUHrzu25nCzU+7YkmfW9KPOWpAxsL8BZ2OK1kGNzy3Kfh3GBWQa7iaz6s8Ro5DOEVncjgrze3DDcGYOzvLjSolIoqTPZoIiIvZ6eSyNvDolWyaucA6XrEzLdbv1objUd7KX4T8y+ZgCDQ7ymY7BgQWP1nfekNrhpr/wpXzHq9Urd8b3racDaKBw7ZNBOTbR/MwP82lr0YJONCrKjqfphCpiymYcIHL4n25gstkh/Ukri4l5kD41sm83fibvtYXalqDgLHjVkkNxeuE4cbjFbB0Sia9iQcCcJW/9cMlmyCJfoC1Ds/jVIvKuQGPEXd3W+/wZ+TRMvNMYNn2JP7gw/HmvFtEh24MenWsGQmiybYrZg3+nCwManUHO3xiKrX6sHqOQTm/F/dErSVawZ79/mugYbGCx3gpz5Jz4JHOliGFJl0CXDrOY6aAKvmNpED6uk9Yzi06wLPh9YyT+BUIuplY14ewUQlfGwEvgapCiYmDwnoi4MUc/0C6+aG0R0LmC6xIyMW278tPdjFwUbgGC+AE+Tfxc304tSw2foLZ+SzcKkCUmlxnZObE8ZEx7Kwa2lFKZ+kTNMz0Yi+fKZJ6rAVV8xHlLB33RuTdLrTCyBqqLynFjeRvZpV1wHNQFuKeQL0wX6yPWwBdlMASuwSSyGDTLE=" 19 | 20 | cache: 21 | directories: 22 | - vendor 23 | 24 | before_install: 25 | - go get -u golang.org/x/lint/golint 26 | 27 | script: 28 | - make ci-test 29 | 30 | after_success: 31 | - bash <(curl -s https://codecov.io/bash) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017- Yuki Iwanaga 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | 3 | SHELL := /bin/bash -eu -o pipefail 4 | 5 | GO_TEST_FLAGS := -v 6 | 7 | PACKAGE_DIRS := $(shell go list ./... 2> /dev/null | grep -v /vendor/) 8 | SRC_FILES := $(shell find . -name '*.go' -not -path './vendor/*') 9 | 10 | 11 | # Tasks 12 | #----------------------------------------------- 13 | .PHONY: lint 14 | lint: 15 | @gofmt -e -d -s $(SRC_FILES) | awk '{ e = 1; print $0 } END { if (e) exit(1) }' 16 | @echo $(SRC_FILES) | xargs -n1 golint -set_exit_status 17 | @go vet $(PACKAGE_DIRS) 18 | 19 | .PHONY: test 20 | test: lint 21 | @go test $(GO_TEST_FLAGS) $(PACKAGE_DIRS) 22 | 23 | .PHONY: ci-test 24 | ci-test: lint 25 | @echo > coverage.txt 26 | @for d in $(PACKAGE_DIRS); do \ 27 | go test -coverprofile=profile.out -covermode=atomic -race -v $$d; \ 28 | if [ -f profile.out ]; then \ 29 | cat profile.out >> coverage.txt; \ 30 | rm profile.out; \ 31 | fi; \ 32 | done 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | fail 2 | ==== 3 | 4 | [![Build Status](https://travis-ci.com/srvc/fail.svg?branch=master)](https://travis-ci.com/srvc/fail) 5 | [![codecov](https://codecov.io/gh/srvc/fail/branch/master/graph/badge.svg)](https://codecov.io/gh/srvc/fail) 6 | [![GoDoc](https://godoc.org/github.com/srvc/fail?status.svg)](https://godoc.org/github.com/srvc/fail) 7 | [![Go project version](https://badge.fury.io/go/github.com%2Fsrvc%2Ffail.svg)](https://badge.fury.io/go/github.com%2Fsrvc%2Ffail) 8 | [![Go Report Card](https://goreportcard.com/badge/github.com/srvc/fail)](https://goreportcard.com/report/github.com/srvc/fail) 9 | [![License](https://img.shields.io/github/license/srvc/fail.svg)](./LICENSE) 10 | 11 | Better error handling solution especially for application servers. 12 | 13 | `fail` provides contextual metadata to errors. 14 | 15 | - Stack trace 16 | - Error code (to express HTTP/gRPC status code) 17 | - Reportability (to integrate with error reporting services) 18 | - Additional information (tags and params) 19 | 20 | 21 | Why 22 | --- 23 | 24 | Since `error` type in Golang is just an interface of [`Error()`](https://golang.org/ref/spec#Errors) 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. 25 | Because of this lack of contextual metadata, debugging is a pain in the ass. 26 | 27 | 32 | 33 | 34 | Create an error 35 | --------------- 36 | 37 | ```go 38 | func New(str string) error 39 | ``` 40 | 41 | New returns an error that formats as the given text. 42 | It also records the stack trace at the point it was called. 43 | 44 | ```go 45 | func Errorf(format string, args ...interface{}) error 46 | ``` 47 | 48 | Errorf formats according to a format specifier and returns the string 49 | as a value that satisfies error. 50 | It also records the stack trace at the point it was called. 51 | 52 | ```go 53 | func Wrap(err error, annotators ...Annotator) error 54 | ``` 55 | 56 | Wrap returns an error annotated with a stack trace from the point it was called, 57 | and with the specified options. 58 | It returns nil if err is nil. 59 | 60 | ### Example: Creating a new error 61 | 62 | ```go 63 | ok := emailRegexp.MatchString("invalid#email.addr") 64 | if !ok { 65 | return fail.New("invalid email address") 66 | } 67 | ``` 68 | 69 | ### Example: Creating from an existing error 70 | 71 | ```go 72 | _, err := ioutil.ReadAll(r) 73 | if err != nil { 74 | return fail.Wrap(err) 75 | } 76 | ``` 77 | 78 | 79 | Annotate an error 80 | ----------------- 81 | 82 | ```go 83 | func WithMessage(msg string) Annotator 84 | ``` 85 | 86 | WithMessage annotates an error with the message. 87 | 88 | ```go 89 | func WithMessagef(msg string, args ...interface{}) Annotator 90 | ``` 91 | 92 | WithMessagef annotates an error with the formatted message. 93 | 94 | ```go 95 | func WithCode(code interface{}) Annotator 96 | ``` 97 | 98 | WithCode annotates an error with the code. 99 | 100 | ```go 101 | func WithIgnorable() Annotator 102 | ``` 103 | 104 | WithIgnorable annotates an error with the reportability. 105 | 106 | ```go 107 | func WithTags(tags ...string) Annotator 108 | ``` 109 | 110 | WithTags annotates an error with tags. 111 | 112 | ```go 113 | func WithParam(key string, value interface{}) Annotator 114 | ``` 115 | 116 | WithParam annotates an error with a key-value pair. 117 | 118 | ```go 119 | // H represents a JSON-like key-value object. 120 | type H map[string]interface{} 121 | 122 | func WithParams(h H) Annotator 123 | ``` 124 | 125 | WithParams annotates an error with key-value pairs. 126 | 127 | 128 | ### Example: Adding all contexts 129 | 130 | ```go 131 | _, err := ioutil.ReadAll(r) 132 | if err != nil { 133 | return fail.Wrap( 134 | err, 135 | fail.WithMessage("read failed"), 136 | fail.WithCode(http.StatusBadRequest), 137 | fail.WithIgnorable(), 138 | ) 139 | } 140 | ``` 141 | 142 | 143 | Extract context from an error 144 | ----------------------------- 145 | 146 | ```go 147 | func Unwrap(err error) *Error 148 | ``` 149 | 150 | Unwrap extracts an underlying \*fail.Error from an error. 151 | If the given error isn't eligible for retriving context from, 152 | it returns nil 153 | 154 | ```go 155 | // Error is an error that has contextual metadata 156 | type Error struct { 157 | // Err is the original error (you might call it the root cause) 158 | Err error 159 | // Messages is an annotated description of the error 160 | Messages []string 161 | // Code is a status code that is desired to be contained in responses, such as HTTP Status code. 162 | Code interface{} 163 | // Ignorable represents whether the error should be reported to administrators 164 | Ignorable bool 165 | // Tags represents tags of the error which is classified errors. 166 | Tags []string 167 | // Params is an annotated parameters of the error. 168 | Params H 169 | // StackTrace is a stack trace of the original error 170 | // from the point where it was created 171 | StackTrace StackTrace 172 | } 173 | ``` 174 | 175 | ### Example 176 | 177 | Here's a minimum executable example illustrating how `fail` works. 178 | 179 | ```go 180 | package main 181 | 182 | import ( 183 | "errors" 184 | 185 | "github.com/k0kubun/pp" 186 | "github.com/srvc/fail/v4" 187 | ) 188 | 189 | var myErr = fail.New("this is the root cause") 190 | 191 | //----------------------------------------------- 192 | type example1 struct{} 193 | 194 | func (e example1) func0() error { 195 | return errors.New("error from third party") 196 | } 197 | func (e example1) func1() error { 198 | return fail.Wrap(e.func0()) 199 | } 200 | func (e example1) func2() error { 201 | return fail.Wrap(e.func1(), fail.WithMessage("fucked up!")) 202 | } 203 | func (e example1) func3() error { 204 | return fail.Wrap(e.func2(), fail.WithCode(500), fail.WithIgnorable()) 205 | } 206 | 207 | //----------------------------------------------- 208 | type example2 struct{} 209 | 210 | func (e example2) func0() error { 211 | return fail.Wrap(myErr) 212 | } 213 | func (e example2) func1() chan error { 214 | c := make(chan error) 215 | go func() { 216 | c <- fail.Wrap(e.func0(), fail.WithTags("async")) 217 | }() 218 | return c 219 | } 220 | func (e example2) func2() error { 221 | return fail.Wrap(<-e.func1(), fail.WithParam("key", 1)) 222 | } 223 | func (e example2) func3() chan error { 224 | c := make(chan error) 225 | go func() { 226 | c <- fail.Wrap(e.func2()) 227 | }() 228 | return c 229 | } 230 | 231 | //----------------------------------------------- 232 | func main() { 233 | { 234 | err := (example1{}).func3() 235 | pp.Println(err) 236 | } 237 | 238 | { 239 | err := <-(example2{}).func3() 240 | pp.Println(err) 241 | } 242 | } 243 | ``` 244 | 245 | ```go 246 | &fail.Error{ 247 | Err: &errors.errorString{s: "error from third party"}, 248 | Messages: []string{"fucked up!"}, 249 | Code: 500, 250 | Ignorable: true, 251 | Tags: []string{}, 252 | Params: fail.H{}, 253 | StackTrace: fail.StackTrace{ 254 | fail.Frame{Func: "example1.func1", File: "stack/main.go", Line: 20}, 255 | fail.Frame{Func: "example1.func2", File: "stack/main.go", Line: 23}, 256 | fail.Frame{Func: "example1.func3", File: "stack/main.go", Line: 26}, 257 | fail.Frame{Func: "main", File: "stack/main.go", Line: 58}, 258 | }, 259 | } 260 | &fail.Error{ 261 | Err: &errors.errorString{s: "this is the root cause"}, 262 | Messages: []string{}, 263 | Code: nil, 264 | Ignorable: false, 265 | Tags: []string{"async"}, 266 | Params: {"key": 1}, 267 | StackTrace: fail.StackTrace{ 268 | fail.Frame{Func: "init", File: "stack/main.go", Line: 10}, 269 | fail.Frame{Func: "example2.func0", File: "stack/main.go", Line: 34}, 270 | fail.Frame{Func: "example2.func1.func1", File: "stack/main.go", Line: 39}, 271 | fail.Frame{Func: "example2.func2", File: "stack/main.go", Line: 44}, 272 | fail.Frame{Func: "example2.func3.func1", File: "stack/main.go", Line: 49}, 273 | fail.Frame{Func: "main", File: "stack/main.go", Line: 64}, 274 | }, 275 | } 276 | ``` 277 | 278 | ### Example: Server-side error reporting with [gin-gonic/gin](https://github.com/gin-gonic/gin) 279 | 280 | Prepare a simple middleware and modify to satisfy your needs: 281 | 282 | ```go 283 | package middleware 284 | 285 | import ( 286 | "net/http" 287 | 288 | "github.com/srvc/fail/v4" 289 | "github.com/creasty/gin-contrib/readbody" 290 | "github.com/gin-gonic/gin" 291 | 292 | // Only for example 293 | "github.com/jinzhu/gorm" 294 | "github.com/k0kubun/pp" 295 | ) 296 | 297 | // ReportError handles an error, changes status code based on the error, 298 | // and reports to an external service if necessary 299 | func ReportError(c *gin.Context, err error) { 300 | failErr := fail.Unwrap(err) 301 | if failErr == nil { 302 | // As it's a "raw" error, `StackTrace` field left unset. 303 | // And it should be always reported 304 | failErr = &fail.Error{ 305 | Err: err, 306 | } 307 | } 308 | 309 | convertFailError(failErr) 310 | 311 | // Send the error to an external service 312 | if !failErr.Ignorable { 313 | go uploadFailError(c.Copy(), failErr) 314 | } 315 | 316 | // Expose an error message in the header 317 | if msg := failErr.LastMessage(); msg != "" { 318 | c.Header("X-App-Error", msg) 319 | } 320 | 321 | // Set status code accordingly 322 | switch code := failErr.Code.(type) { 323 | case int: 324 | c.Status(code) 325 | default: 326 | c.Status(http.StatusInternalServerError) 327 | } 328 | } 329 | 330 | func convertFailError(err *fail.Error) { 331 | // If the error is from ORM and it says "no record found," 332 | // override status code to 404 333 | if err.Err == gorm.ErrRecordNotFound { 334 | err.Code = http.StatusNotFound 335 | return 336 | } 337 | } 338 | 339 | func uploadFailError(c *gin.Context, err *fail.Error) { 340 | // By using readbody, you can retrive an original request body 341 | // even when c.Request.Body had been read 342 | body := readbody.Get(c) 343 | 344 | // Just debug 345 | pp.Println(string(body[:])) 346 | pp.Println(err) 347 | } 348 | ``` 349 | 350 | And then you can use like as follows. 351 | 352 | ```go 353 | r := gin.Default() 354 | r.Use(readbody.Recorder()) // Use github.com/creasty/gin-contrib/readbody 355 | 356 | r.GET("/test", func(c *gin.Context) { 357 | err := doSomethingReallyComplex() 358 | if err != nil { 359 | middleware.ReportError(c, err) // Neither `c.AbortWithError` nor `c.Error` 360 | return 361 | } 362 | 363 | c.Status(200) 364 | }) 365 | 366 | r.Run() 367 | ``` 368 | -------------------------------------------------------------------------------- /annotators.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import "fmt" 4 | 5 | // Annotator is a function that annotates an error with information 6 | type Annotator func(*Error) 7 | 8 | // WithMessage annotates an error with the message 9 | func WithMessage(msg string) Annotator { 10 | return func(err *Error) { 11 | if msg == "" { 12 | return 13 | } 14 | err.Messages = append([]string{msg}, err.Messages...) 15 | } 16 | } 17 | 18 | // WithMessagef annotates an error with the formatted message 19 | func WithMessagef(msg string, args ...interface{}) Annotator { 20 | return WithMessage(fmt.Sprintf(msg, args...)) 21 | } 22 | 23 | // WithCode annotates an error with the code 24 | func WithCode(code interface{}) Annotator { 25 | return func(err *Error) { 26 | err.Code = code 27 | } 28 | } 29 | 30 | // WithIgnorable annotates an error with the reportability 31 | func WithIgnorable() Annotator { 32 | return func(err *Error) { 33 | err.Ignorable = true 34 | } 35 | } 36 | 37 | // WithTags annotates an error with tags 38 | func WithTags(tags ...string) Annotator { 39 | return func(err *Error) { 40 | err.Tags = append(err.Tags, tags...) 41 | } 42 | } 43 | 44 | // WithParam annotates an error with a key-value pair 45 | func WithParam(key string, value interface{}) Annotator { 46 | return WithParams(H{key: value}) 47 | } 48 | 49 | // WithParams annotates an error with key-value pairs 50 | func WithParams(h H) Annotator { 51 | return func(err *Error) { 52 | err.Params = err.Params.Merge(h) 53 | } 54 | } 55 | 56 | // withStackTrace annotates an error with the stack trace from the point it was called 57 | func withStackTrace(offset int) Annotator { 58 | stackTrace := newStackTrace(offset + 1) 59 | 60 | return func(err *Error) { 61 | err.StackTrace = mergeStackTraces(err.StackTrace, stackTrace) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | const ( 10 | messageDelimiter = ": " 11 | ) 12 | 13 | // Error is an error that has contextual metadata 14 | type Error struct { 15 | // Err is the original error (you might call it the root cause) 16 | Err error 17 | // Messages is an annotated description of the error 18 | Messages []string 19 | // Code is a status code that is desired to be contained in responses, such as HTTP Status code. 20 | Code interface{} 21 | // Ignorable represents whether the error should be reported to administrators 22 | Ignorable bool 23 | // Tags represents tags of the error which is classified errors. 24 | Tags []string 25 | // Params is an annotated parameters of the error. 26 | Params H 27 | // StackTrace is a stack trace of the original error 28 | // from the point where it was created 29 | StackTrace StackTrace 30 | } 31 | 32 | // New returns an error that formats as the given text. 33 | // It also records the stack trace at the point it was called. 34 | func New(text string) error { 35 | err := &Error{Err: errors.New(text)} 36 | withStackTrace(0)(err) 37 | return err 38 | } 39 | 40 | // Errorf formats according to a format specifier and returns the string 41 | // as a value that satisfies error. 42 | // It also records the stack trace at the point it was called. 43 | func Errorf(format string, args ...interface{}) error { 44 | err := &Error{Err: fmt.Errorf(format, args...)} 45 | withStackTrace(0)(err) 46 | return err 47 | } 48 | 49 | // Error implements error interface. 50 | // It returns a string of messages and the root error concatenated with ": ". 51 | func (e *Error) Error() string { 52 | messages := append(e.Messages, e.Err.Error()) 53 | return strings.Join(messages, messageDelimiter) 54 | } 55 | 56 | // Copy creates a copy of the current object 57 | func (e *Error) Copy() *Error { 58 | return &Error{ 59 | Err: e.Err, 60 | Messages: e.Messages, 61 | Code: e.Code, 62 | Ignorable: e.Ignorable, 63 | Tags: e.Tags, 64 | Params: e.Params, 65 | StackTrace: e.StackTrace, 66 | } 67 | } 68 | 69 | // LastMessage returns the last message 70 | func (e *Error) LastMessage() string { 71 | if len(e.Messages) == 0 { 72 | return "" 73 | } 74 | return e.Messages[0] 75 | } 76 | 77 | // Unwrap provides compatibility for Go 1.13 error chains. 78 | func (e *Error) Unwrap() error { return e.Err } 79 | 80 | // Wrap returns an error annotated with a stack trace from the point it was called, 81 | // and with the specified annotators. 82 | // It returns nil if err is nil. 83 | func Wrap(err error, annotators ...Annotator) error { 84 | if err == nil { 85 | return nil 86 | } 87 | 88 | failErr := Unwrap(err) 89 | if failErr == nil { 90 | failErr = &Error{ 91 | Err: err, 92 | } 93 | } 94 | 95 | withStackTrace(0)(failErr) 96 | 97 | for _, f := range annotators { 98 | f(failErr) 99 | } 100 | 101 | return failErr 102 | } 103 | 104 | // Unwrap extracts an underlying *fail.Error from an error. 105 | // If the given error isn't eligible for retriving context from, 106 | // it returns nil 107 | func Unwrap(err error) *Error { 108 | if err == nil { 109 | return nil 110 | } 111 | 112 | if failErr, ok := err.(*Error); ok { 113 | return failErr.Copy() 114 | } 115 | 116 | if failErr := convertPkgError(err); failErr != nil { 117 | return failErr 118 | } 119 | 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /error_go113_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.13 2 | 3 | package fail 4 | 5 | import ( 6 | "errors" 7 | "testing" 8 | ) 9 | 10 | func TestError_Unwrap(t *testing.T) { 11 | err := errFunc0e() 12 | failErr := Wrap(err, WithMessage("wrapped")) 13 | if !errors.Is(failErr, err) { 14 | t.Errorf("underlying error should be %v", err) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /error_test.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | pkgerrors "github.com/pkg/errors" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNew(t *testing.T) { 12 | err := New("err") 13 | assert.Equal(t, "err", err.Error()) 14 | 15 | failErr := Unwrap(err) 16 | assert.Equal(t, "err", failErr.Error()) 17 | assert.NotEmpty(t, failErr.StackTrace) 18 | assert.Equal(t, "TestNew", failErr.StackTrace[0].Func) 19 | } 20 | 21 | func TestErrorf(t *testing.T) { 22 | err := Errorf("err %d", 123) 23 | assert.Equal(t, "err 123", err.Error()) 24 | 25 | failErr := Unwrap(err) 26 | assert.Equal(t, "err 123", failErr.Error()) 27 | assert.NotEmpty(t, failErr.StackTrace) 28 | assert.Equal(t, "TestErrorf", failErr.StackTrace[0].Func) 29 | } 30 | 31 | func TestError_LastMessage(t *testing.T) { 32 | err := &Error{ 33 | Err: errors.New("err"), 34 | Messages: []string{"message 2", "message 1"}, 35 | } 36 | assert.Equal(t, "message 2", err.LastMessage()) 37 | } 38 | 39 | func TestWithMessage(t *testing.T) { 40 | t.Run("nil", func(t *testing.T) { 41 | err := Wrap(nil, WithMessage("message")) 42 | assert.Equal(t, nil, err) 43 | }) 44 | 45 | t.Run("bare", func(t *testing.T) { 46 | err0 := errors.New("origin") 47 | 48 | err1 := Wrap(err0, WithMessage("message")) 49 | assert.Equal(t, "message: origin", err1.Error()) 50 | 51 | failErr := Unwrap(err1) 52 | assert.Equal(t, err0, failErr.Err) 53 | assert.Equal(t, err1.Error(), failErr.Error()) 54 | }) 55 | 56 | t.Run("already wrapped", func(t *testing.T) { 57 | err0 := errors.New("origin") 58 | 59 | err1 := &Error{ 60 | Err: err0, 61 | Messages: []string{"message 1"}, 62 | Code: 400, 63 | } 64 | err2 := Wrap(err1, WithMessage("message 2")) 65 | assert.Equal(t, "message 2: message 1: origin", err2.Error()) 66 | 67 | { 68 | failErr := Unwrap(err1) 69 | assert.Equal(t, err0, failErr.Err) 70 | assert.Equal(t, err1.Error(), failErr.Error()) 71 | assert.Equal(t, 400, failErr.Code) 72 | } 73 | 74 | { 75 | failErr := Unwrap(err2) 76 | assert.Equal(t, err0, failErr.Err) 77 | assert.Equal(t, err2.Error(), failErr.Error()) 78 | assert.Equal(t, 400, failErr.Code) 79 | } 80 | }) 81 | } 82 | 83 | func TestWithMessagef(t *testing.T) { 84 | t.Run("nil", func(t *testing.T) { 85 | err := Wrap(nil, WithMessagef("message %d", 1)) 86 | assert.Equal(t, nil, err) 87 | }) 88 | 89 | t.Run("bare", func(t *testing.T) { 90 | err0 := errors.New("origin") 91 | 92 | err1 := Wrap(err0, WithMessagef("message %d", 1)) 93 | assert.Equal(t, "message 1: origin", err1.Error()) 94 | 95 | failErr := Unwrap(err1) 96 | assert.Equal(t, err0, failErr.Err) 97 | assert.Equal(t, err1.Error(), failErr.Error()) 98 | }) 99 | 100 | t.Run("already wrapped", func(t *testing.T) { 101 | err0 := errors.New("origin") 102 | 103 | err1 := &Error{ 104 | Err: err0, 105 | Messages: []string{"message 1"}, 106 | Code: 400, 107 | } 108 | err2 := Wrap(err1, WithMessagef("message %d", 2)) 109 | assert.Equal(t, "message 2: message 1: origin", err2.Error()) 110 | 111 | { 112 | failErr := Unwrap(err1) 113 | assert.Equal(t, err0, failErr.Err) 114 | assert.Equal(t, err1.Error(), failErr.Error()) 115 | assert.Equal(t, 400, failErr.Code) 116 | } 117 | 118 | { 119 | failErr := Unwrap(err2) 120 | assert.Equal(t, err0, failErr.Err) 121 | assert.Equal(t, err2.Error(), failErr.Error()) 122 | assert.Equal(t, 400, failErr.Code) 123 | } 124 | }) 125 | } 126 | 127 | func TestWithCode(t *testing.T) { 128 | t.Run("nil", func(t *testing.T) { 129 | err := Wrap(nil, WithCode(200)) 130 | assert.Equal(t, nil, err) 131 | }) 132 | 133 | t.Run("bare", func(t *testing.T) { 134 | err0 := errors.New("origin") 135 | 136 | err1 := Wrap(err0, WithCode(200)) 137 | 138 | failErr := Unwrap(err1) 139 | assert.Equal(t, err0, failErr.Err) 140 | assert.Equal(t, "origin", failErr.Error()) 141 | }) 142 | 143 | t.Run("already wrapped", func(t *testing.T) { 144 | err0 := errors.New("origin") 145 | 146 | err1 := &Error{ 147 | Err: err0, 148 | Messages: []string{"message 1"}, 149 | Code: 400, 150 | } 151 | err2 := Wrap(err1, WithCode(500)) 152 | 153 | { 154 | failErr := Unwrap(err1) 155 | assert.Equal(t, err0, failErr.Err) 156 | assert.Equal(t, "message 1: origin", failErr.Error()) 157 | assert.Equal(t, 400, failErr.Code) 158 | } 159 | 160 | { 161 | failErr := Unwrap(err2) 162 | assert.Equal(t, err0, failErr.Err) 163 | assert.Equal(t, "message 1: origin", failErr.Error()) 164 | assert.Equal(t, 500, failErr.Code) 165 | } 166 | }) 167 | } 168 | 169 | func TestWithTags(t *testing.T) { 170 | t.Run("nil", func(t *testing.T) { 171 | err := Wrap(nil, WithTags("http", "notice_only")) 172 | assert.Equal(t, nil, err) 173 | }) 174 | 175 | t.Run("bare", func(t *testing.T) { 176 | err0 := errors.New("origin") 177 | 178 | err1 := Wrap(err0, WithTags("http", "notice_only")) 179 | 180 | failErr := Unwrap(err1) 181 | assert.Equal(t, err0, failErr.Err) 182 | assert.Equal(t, []string{"http", "notice_only"}, failErr.Tags) 183 | }) 184 | 185 | t.Run("already wrapped", func(t *testing.T) { 186 | err0 := errors.New("origin") 187 | 188 | err1 := Wrap(err0, WithTags("http", "notice_only")) 189 | err2 := Wrap(err1, WithTags("security")) 190 | 191 | { 192 | failErr := Unwrap(err1) 193 | assert.Equal(t, err0, failErr.Err) 194 | assert.Equal(t, []string{"http", "notice_only"}, failErr.Tags) 195 | } 196 | 197 | { 198 | failErr := Unwrap(err2) 199 | assert.Equal(t, err0, failErr.Err) 200 | assert.Equal(t, []string{"http", "notice_only", "security"}, failErr.Tags) 201 | } 202 | }) 203 | } 204 | 205 | func TestWithParams(t *testing.T) { 206 | t.Run("nil", func(t *testing.T) { 207 | err := Wrap(nil, WithParams(H{"foo": 1, "bar": "baz"})) 208 | assert.Equal(t, nil, err) 209 | }) 210 | 211 | t.Run("bare", func(t *testing.T) { 212 | err0 := errors.New("origin") 213 | 214 | err1 := Wrap(err0, WithParams(H{"foo": 1, "bar": "baz"})) 215 | 216 | failErr := Unwrap(err1) 217 | assert.Equal(t, err0, failErr.Err) 218 | assert.Equal(t, H{"foo": 1, "bar": "baz"}, failErr.Params) 219 | }) 220 | 221 | t.Run("short", func(t *testing.T) { 222 | err0 := errors.New("origin") 223 | 224 | err1 := Wrap(err0, WithParam("foo", 1)) 225 | 226 | failErr := Unwrap(err1) 227 | assert.Equal(t, err0, failErr.Err) 228 | assert.Equal(t, H{"foo": 1}, failErr.Params) 229 | }) 230 | 231 | t.Run("already wrapped", func(t *testing.T) { 232 | err0 := errors.New("origin") 233 | 234 | err1 := Wrap(err0, WithParams(H{"foo": 1, "bar": "baz"})) 235 | err2 := Wrap(err1, WithParams(H{"qux": true, "foo": "quux"})) 236 | 237 | { 238 | failErr := Unwrap(err1) 239 | assert.Equal(t, err0, failErr.Err) 240 | assert.Equal(t, H{"foo": 1, "bar": "baz"}, failErr.Params) 241 | } 242 | 243 | { 244 | failErr := Unwrap(err2) 245 | assert.Equal(t, err0, failErr.Err) 246 | assert.Equal(t, H{"foo": "quux", "bar": "baz", "qux": true}, failErr.Params) 247 | } 248 | }) 249 | } 250 | 251 | func TestWithIgnorable(t *testing.T) { 252 | t.Run("nil", func(t *testing.T) { 253 | err := Wrap(nil, WithIgnorable()) 254 | assert.Equal(t, nil, err) 255 | }) 256 | 257 | t.Run("bare", func(t *testing.T) { 258 | err0 := errors.New("origin") 259 | 260 | err1 := Wrap(err0, WithIgnorable()) 261 | 262 | failErr := Unwrap(err1) 263 | assert.Equal(t, err0, failErr.Err) 264 | assert.Equal(t, "origin", failErr.Error()) 265 | }) 266 | 267 | t.Run("already wrapped", func(t *testing.T) { 268 | err0 := errors.New("origin") 269 | 270 | err1 := Wrap(err0, WithIgnorable()) 271 | err2 := Wrap(err1, WithIgnorable()) 272 | 273 | { 274 | failErr := Unwrap(err1) 275 | assert.Equal(t, err0, failErr.Err) 276 | assert.Equal(t, true, failErr.Ignorable) 277 | } 278 | 279 | { 280 | failErr := Unwrap(err2) 281 | assert.Equal(t, err0, failErr.Err) 282 | assert.Equal(t, true, failErr.Ignorable) 283 | } 284 | }) 285 | } 286 | 287 | func TestUnwrap(t *testing.T) { 288 | t.Run("nil", func(t *testing.T) { 289 | failErr := Unwrap(nil) 290 | assert.Nil(t, failErr) 291 | }) 292 | } 293 | 294 | func TestWrap(t *testing.T) { 295 | t.Run("nil", func(t *testing.T) { 296 | failErr := Wrap(nil) 297 | assert.Nil(t, failErr) 298 | }) 299 | 300 | t.Run("bare", func(t *testing.T) { 301 | err0 := errors.New("origin") 302 | 303 | err1 := wrapOrigin(err0) 304 | assert.Equal(t, "origin", err1.Error()) 305 | 306 | failErr := Unwrap(err1) 307 | assert.Equal(t, err0, failErr.Err) 308 | assert.Equal(t, "origin", failErr.Error()) 309 | assert.NotEmpty(t, failErr.StackTrace) 310 | assert.Equal(t, "wrapOrigin", failErr.StackTrace[0].Func) 311 | }) 312 | 313 | t.Run("already wrapped", func(t *testing.T) { 314 | err0 := errors.New("origin") 315 | 316 | err1 := wrapOrigin(err0) 317 | err2 := wrapOrigin(err1) 318 | assert.Equal(t, "origin", err2.Error()) 319 | 320 | failErr := Unwrap(err2) 321 | assert.Equal(t, err0, failErr.Err) 322 | assert.Equal(t, "origin", failErr.Error()) 323 | assert.NotEmpty(t, failErr.StackTrace) 324 | assert.Equal(t, "wrapOrigin", failErr.StackTrace[0].Func) 325 | }) 326 | 327 | t.Run("with pkg/errors", func(t *testing.T) { 328 | t.Run("pkg/errors.New", func(t *testing.T) { 329 | err0 := pkgErrorsNew("origin") 330 | 331 | err1 := wrapOrigin(err0) 332 | assert.Equal(t, "origin", err1.Error()) 333 | 334 | failErr := Unwrap(err1) 335 | assert.Equal(t, err0, failErr.Err) 336 | assert.Equal(t, "origin", failErr.Error()) 337 | assert.NotEmpty(t, failErr.StackTrace) 338 | assert.Equal(t, "pkgErrorsNew", failErr.StackTrace[0].Func) 339 | }) 340 | 341 | t.Run("pkg/errors.Wrap", func(t *testing.T) { 342 | err0 := errors.New("origin") 343 | err1 := pkgErrorsWrap(err0, "message") 344 | 345 | err2 := wrapOrigin(err1) 346 | assert.Equal(t, "message: origin", err2.Error()) 347 | 348 | failErr := Unwrap(err2) 349 | assert.Equal(t, err0, failErr.Err) 350 | assert.Equal(t, "message: origin", failErr.Error()) 351 | assert.NotEmpty(t, failErr.StackTrace) 352 | assert.Equal(t, "pkgErrorsWrap", failErr.StackTrace[0].Func) 353 | }) 354 | }) 355 | } 356 | 357 | func TestAll(t *testing.T) { 358 | t.Run("e-p-p-f", func(t *testing.T) { 359 | failErr := Unwrap(errFunc0e1p2p3f()) 360 | assert.Equal(t, "2p: 1p: 0e", failErr.Error()) 361 | assert.Equal(t, nil, failErr.Code) 362 | assert.Equal(t, false, failErr.Ignorable) 363 | assert.Equal(t, []string{ 364 | "errFunc0e1p", 365 | "errFunc0e1p2p", 366 | "errFunc0e1p2p3f", 367 | "TestAll.func1", 368 | "tRunner", 369 | }, funcNamesFromStackTrace(failErr.StackTrace)) 370 | }) 371 | 372 | t.Run("e-p-p-f-f", func(t *testing.T) { 373 | failErr := Unwrap(errFunc0e1p2p3f4f()) 374 | assert.Equal(t, "4f: 2p: 1p: 0e", failErr.Error()) 375 | assert.Equal(t, 500, failErr.Code) 376 | assert.Equal(t, true, failErr.Ignorable) 377 | assert.Equal(t, []string{ 378 | "errFunc0e1p", 379 | "errFunc0e1p2p", 380 | "errFunc0e1p2p3f", 381 | "errFunc0e1p2p3f4f", 382 | "TestAll.func2", 383 | "tRunner", 384 | }, funcNamesFromStackTrace(failErr.StackTrace)) 385 | }) 386 | 387 | t.Run("e-p-p-fg-f", func(t *testing.T) { 388 | failErr := Unwrap(errFunc0e1p2p3fg4f()) 389 | assert.Equal(t, "4f: 2p: 1p: 0e", failErr.Error()) 390 | assert.Equal(t, 500, failErr.Code) 391 | assert.Equal(t, true, failErr.Ignorable) 392 | assert.Equal(t, []string{ 393 | "errFunc0e1p", 394 | "errFunc0e1p2p", 395 | "errFunc0e1p2p3fg.func1", 396 | "errFunc0e1p2p3fg4f", 397 | "TestAll.func3", 398 | "tRunner", 399 | }, funcNamesFromStackTrace(failErr.StackTrace)) 400 | }) 401 | 402 | t.Run("e-p-p-f-p", func(t *testing.T) { 403 | failErr := Unwrap(errFunc0e1p2p3f4p()) 404 | assert.Equal(t, "4p: 2p: 1p: 0e", failErr.Error()) 405 | assert.Equal(t, []string{ 406 | "errFunc0e1p", 407 | "errFunc0e1p2p", 408 | "errFunc0e1p2p3f", 409 | "errFunc0e1p2p3f4p", 410 | "TestAll.func4", 411 | "tRunner", 412 | }, funcNamesFromStackTrace(failErr.StackTrace)) 413 | }) 414 | 415 | t.Run("e-p-p-fg-f-p", func(t *testing.T) { 416 | failErr := Unwrap(errFunc0e1p2p3fg4f5p()) 417 | assert.Equal(t, "5p: 4f: 2p: 1p: 0e", failErr.Error()) 418 | assert.Equal(t, []string{ 419 | "errFunc0e1p", 420 | "errFunc0e1p2p", 421 | "errFunc0e1p2p3fg.func1", 422 | "errFunc0e1p2p3fg4f", 423 | "errFunc0e1p2p3fg4f5p", 424 | "TestAll.func5", 425 | "tRunner", 426 | }, funcNamesFromStackTrace(failErr.StackTrace)) 427 | }) 428 | 429 | t.Run("e-p-p-pg-p", func(t *testing.T) { 430 | failErr := Unwrap(errFunc0e1p2p3pg4p()) 431 | assert.Equal(t, "4p: 3pg: 2p: 1p: 0e", failErr.Error()) 432 | assert.Equal(t, []string{ 433 | "errFunc0e1p", 434 | "errFunc0e1p2p", 435 | "errFunc0e1p2p3pg.func1", 436 | "errFunc0e1p2p3pg4p", 437 | "TestAll.func6", 438 | "tRunner", 439 | }, funcNamesFromStackTrace(failErr.StackTrace)) 440 | }) 441 | } 442 | 443 | func wrapOrigin(err error) error { 444 | return Wrap(err) 445 | } 446 | 447 | func funcNamesFromStackTrace(stackTrace StackTrace) (funcNames []string) { 448 | for _, frame := range stackTrace { 449 | funcNames = append(funcNames, frame.Func) 450 | } 451 | return 452 | } 453 | 454 | // Error functions 455 | // 456 | // How to read: `errFunc0e1p2p3fg4f` 457 | // 458 | // Prefix Error type: e = build-in errors, f = srvc/fail, p = pkg/errors 459 | // | | 460 | // errFunc 0e 1p 2p 3fg 4f 461 | // ^^^^^^^ | | 462 | // Depth Goroutine involved 463 | // 464 | // errors -> pkg/errors -> pkg/errors -> fail (goroutine) -> fail 465 | 466 | func errFunc0e() error { 467 | return errors.New("0e") 468 | } 469 | func errFunc0e1p() error { 470 | return pkgerrors.Wrap(errFunc0e(), "1p") 471 | } 472 | func errFunc0e1p2p() error { 473 | return pkgerrors.Wrap(errFunc0e1p(), "2p") 474 | } 475 | func errFunc0e1p2p3f() error { 476 | return Wrap(errFunc0e1p2p()) 477 | } 478 | func errFunc0e1p2p3f4f() error { 479 | return Wrap(errFunc0e1p2p3f(), WithMessage("4f"), WithCode(500), WithIgnorable()) 480 | } 481 | 482 | func errFunc0e1p2p3fg() chan error { 483 | c := make(chan error) 484 | go func() { 485 | c <- Wrap(errFunc0e1p2p()) 486 | }() 487 | return c 488 | } 489 | func errFunc0e1p2p3fg4f() error { 490 | return Wrap(<-errFunc0e1p2p3fg(), WithMessage("4f"), WithCode(500), WithIgnorable()) 491 | } 492 | func errFunc0e1p2p3fg4f5p() error { 493 | return pkgerrors.Wrap(errFunc0e1p2p3fg4f(), "5p") 494 | } 495 | 496 | func errFunc0e1p2p3pg() chan error { 497 | c := make(chan error) 498 | go func() { 499 | c <- pkgerrors.Wrap(errFunc0e1p2p(), "3pg") 500 | }() 501 | return c 502 | } 503 | func errFunc0e1p2p3pg4p() error { 504 | return pkgerrors.Wrap(<-errFunc0e1p2p3pg(), "4p") 505 | } 506 | 507 | func errFunc0e1p2p3f4p() error { 508 | return pkgerrors.Wrap(errFunc0e1p2p3f(), "4p") 509 | } 510 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/srvc/fail/v4 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/pkg/errors v0.8.1 7 | github.com/stretchr/testify v1.4.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 4 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 9 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 10 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 11 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 12 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 13 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 14 | -------------------------------------------------------------------------------- /hash.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | // H represents a JSON-like key-value object. 4 | type H map[string]interface{} 5 | 6 | // Merge returns a new H object contains self and other H contents. 7 | func (h H) Merge(other map[string]interface{}) H { 8 | out := H{} 9 | 10 | for k, v := range h { 11 | out[k] = v 12 | } 13 | for k, v := range other { 14 | out[k] = v 15 | } 16 | 17 | return out 18 | } 19 | -------------------------------------------------------------------------------- /hash_test.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMergeH(t *testing.T) { 10 | cases := []struct { 11 | test string 12 | left, right, want H 13 | }{ 14 | { 15 | test: "empty", 16 | left: H{}, 17 | right: H{}, 18 | want: H{}, 19 | }, 20 | { 21 | test: "simple", 22 | left: H{"foo": 1}, 23 | right: H{"bar": "baz"}, 24 | want: H{"foo": 1, "bar": "baz"}, 25 | }, 26 | { 27 | test: "overwrite", 28 | left: H{"foo": 1, "bar": "baz"}, 29 | right: H{"qux": true, "foo": "quux"}, 30 | want: H{"foo": "quux", "bar": "baz", "qux": true}, 31 | }, 32 | } 33 | 34 | for _, c := range cases { 35 | t.Run(c.test, func(t *testing.T) { 36 | got := c.left.Merge(c.right) 37 | assert.Equal(t, c.want, got) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /pkgerrors.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | 7 | pkgerrors "github.com/pkg/errors" 8 | ) 9 | 10 | type pkgError struct { 11 | Err error 12 | Messages []string 13 | StackTrace StackTrace 14 | } 15 | 16 | const ( 17 | pkgErrorsMessageDelimiter = ": " 18 | ) 19 | 20 | // convertPkgError converts pkg/errors to fail. 21 | // It returns nil if the error is not derived from pkg/errors. 22 | func convertPkgError(err error) (convertedErr *Error) { 23 | pkgErr := extractPkgError(err) 24 | if pkgErr == nil { 25 | return 26 | } 27 | 28 | if failErr, ok := pkgErr.Err.(*Error); ok { 29 | convertedErr = failErr.Copy() 30 | convertedErr.StackTrace = mergeStackTraces(failErr.StackTrace, pkgErr.StackTrace) 31 | } else { 32 | convertedErr = &Error{ 33 | Err: pkgErr.Err, 34 | StackTrace: pkgErr.StackTrace, 35 | } 36 | } 37 | 38 | for i := len(pkgErr.Messages) - 1; i >= 0; i-- { 39 | WithMessage(pkgErr.Messages[i])(convertedErr) 40 | } 41 | 42 | return 43 | } 44 | 45 | // extractPkgError extracts the innermost error from the given error. 46 | // It converts the stack trace that is annotated by pkg/errors into fail.StackTrace. 47 | // If the error doesn't have a stack trace or a causer of pkg/errors, 48 | // it simply returns the original error 49 | func extractPkgError(err error) *pkgError { 50 | type traceable interface { 51 | StackTrace() pkgerrors.StackTrace 52 | } 53 | type causer interface { 54 | Cause() error 55 | } 56 | 57 | var stackTraces []StackTrace 58 | var messages []string 59 | var lastMessage string 60 | 61 | // Retrive stacks and trace back the root cause 62 | rootErr := err 63 | for { 64 | if t, ok := rootErr.(traceable); ok { 65 | stackTrace := convertStackTrace(t.StackTrace()) 66 | stackTraces = append(stackTraces, stackTrace) 67 | } 68 | 69 | if cause, ok := rootErr.(causer); ok { 70 | msg := rootErr.Error() 71 | if lastMessage != msg { 72 | lastMessage = msg 73 | messages = append(messages, msg) 74 | } 75 | 76 | rootErr = cause.Cause() 77 | continue 78 | } 79 | 80 | break 81 | } 82 | 83 | if len(stackTraces) == 0 { 84 | ret, et := reflect.TypeOf(rootErr), reflect.TypeOf(err) 85 | if ret != nil && et != nil && ret.Comparable() && et.Comparable() { 86 | if rootErr == err { 87 | return nil 88 | } 89 | } else { 90 | return nil 91 | } 92 | } 93 | 94 | // Extract annotated messages by removing the trailing message. 95 | // 96 | // w2 := errors.Wrap(e0, "message 2") // w2.Error() == "mesasge 2: message 1: e0" 97 | // w1 := errors.Wrap(e0, "message 1") // w1.Error() == "message 1: e0" 98 | // e0 := errors.New("e0") // e0.Error() == "e0" 99 | // 100 | // "e0" 101 | // \ 102 | // '-. 103 | // \ 104 | // "message 1: e0" : "e0" --> ": e0" --> "messages 1" 105 | // \ 106 | // '-. 107 | // \ 108 | // "mesasge 2: message 1: e0" : "message 1: e0" --> ": message 1: e0" --> "messages 2" 109 | var cleanedMessages []string 110 | trailingMessage := rootErr.Error() 111 | for i := len(messages) - 1; i >= 0; i-- { 112 | if strings.HasSuffix(messages[i], pkgErrorsMessageDelimiter+trailingMessage) { 113 | trimmed := strings.TrimSuffix(messages[i], pkgErrorsMessageDelimiter+trailingMessage) 114 | trailingMessage = messages[i] 115 | 116 | if trimmed != "" { // Discard empty messages 117 | cleanedMessages = append([]string{trimmed}, cleanedMessages...) 118 | } 119 | } 120 | } 121 | 122 | return &pkgError{ 123 | Err: rootErr, 124 | Messages: cleanedMessages, 125 | StackTrace: reduceStackTraces(stackTraces), 126 | } 127 | } 128 | 129 | // convertStackTrace converts pkg/errors.StackTrace into fail.StackTrace 130 | func convertStackTrace(stackTrace pkgerrors.StackTrace) StackTrace { 131 | pcs := make([]uintptr, len(stackTrace)) 132 | for i, t := range stackTrace { 133 | pcs[i] = uintptr(t) 134 | } 135 | return newStackTraceFromPCs(pcs) 136 | } 137 | -------------------------------------------------------------------------------- /pkgerrors_test.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "testing" 7 | 8 | pkgerrors "github.com/pkg/errors" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestExtractPkgError(t *testing.T) { 14 | t.Run("nil", func(t *testing.T) { 15 | pkgErr := extractPkgError(nil) 16 | assert.Nil(t, pkgErr) 17 | }) 18 | 19 | t.Run("not a pkg/errors", func(t *testing.T) { 20 | pkgErr := extractPkgError(errors.New("error")) 21 | assert.Nil(t, pkgErr) 22 | }) 23 | 24 | t.Run("slice error", func(t *testing.T) { 25 | err := errorSlice{errors.New("error")} 26 | 27 | pkgErr := extractPkgError(err) 28 | assert.Nil(t, pkgErr) 29 | }) 30 | 31 | t.Run("pkg/errors.New", func(t *testing.T) { 32 | err := pkgErrorsNew("message") 33 | 34 | pkgErr := extractPkgError(err) 35 | assert.NotNil(t, pkgErr) 36 | assert.Equal(t, []string(nil), pkgErr.Messages) 37 | assert.Equal(t, err, pkgErr.Err) 38 | assert.NotEmpty(t, pkgErr.StackTrace) 39 | assert.Equal(t, "pkgErrorsNew", pkgErr.StackTrace[0].Func) 40 | }) 41 | 42 | t.Run("pkg/errors.Wrap", func(t *testing.T) { 43 | t.Run("single wrap", func(t *testing.T) { 44 | err0 := errors.New("error") 45 | err1 := pkgErrorsWrap(err0, "message") 46 | 47 | pkgErr := extractPkgError(err1) 48 | assert.NotNil(t, pkgErr) 49 | assert.Equal(t, []string{"message"}, pkgErr.Messages) 50 | assert.Equal(t, err0, pkgErr.Err) 51 | assert.NotEmpty(t, pkgErr.StackTrace) 52 | assert.Equal(t, "pkgErrorsWrap", pkgErr.StackTrace[0].Func) 53 | }) 54 | 55 | t.Run("multiple wrap", func(t *testing.T) { 56 | err0 := errors.New("error") 57 | err1 := pkgErrorsWrap(err0, "message 1") 58 | err2 := pkgErrorsWrap(err1, "message 2") 59 | 60 | pkgErr := extractPkgError(err2) 61 | assert.NotNil(t, pkgErr) 62 | assert.Equal(t, []string{"message 2", "message 1"}, pkgErr.Messages) 63 | assert.Equal(t, err0, pkgErr.Err) 64 | assert.NotEmpty(t, pkgErr.StackTrace) 65 | assert.Equal(t, "pkgErrorsWrap", pkgErr.StackTrace[0].Func) 66 | }) 67 | 68 | t.Run("multiple wrap with an empty message (first)", func(t *testing.T) { 69 | err0 := errors.New("error") 70 | err1 := pkgErrorsWrap(err0, "") 71 | err2 := pkgErrorsWrap(err1, "message 2") 72 | err3 := pkgErrorsWrap(err2, "message 3") 73 | 74 | pkgErr := extractPkgError(err3) 75 | assert.NotNil(t, pkgErr) 76 | assert.Equal(t, []string{"message 3", "message 2"}, pkgErr.Messages) 77 | assert.Equal(t, err0, pkgErr.Err) 78 | assert.NotEmpty(t, pkgErr.StackTrace) 79 | assert.Equal(t, "pkgErrorsWrap", pkgErr.StackTrace[0].Func) 80 | }) 81 | 82 | t.Run("multiple wrap with an empty message (middle)", func(t *testing.T) { 83 | err0 := errors.New("error") 84 | err1 := pkgErrorsWrap(err0, "message 1") 85 | err2 := pkgErrorsWrap(err1, "") 86 | err3 := pkgErrorsWrap(err2, "message 3") 87 | 88 | pkgErr := extractPkgError(err3) 89 | assert.NotNil(t, pkgErr) 90 | assert.Equal(t, []string{"message 3", "message 1"}, pkgErr.Messages) 91 | assert.Equal(t, err0, pkgErr.Err) 92 | assert.NotEmpty(t, pkgErr.StackTrace) 93 | assert.Equal(t, "pkgErrorsWrap", pkgErr.StackTrace[0].Func) 94 | }) 95 | 96 | t.Run("with slice error", func(t *testing.T) { 97 | err0 := errorSlice{errors.New("error")} 98 | err1 := pkgErrorsWrap(err0, "message") 99 | 100 | pkgErr := extractPkgError(err1) 101 | assert.NotNil(t, pkgErr) 102 | assert.Equal(t, err0, pkgErr.Err) 103 | assert.NotEmpty(t, pkgErr.StackTrace) 104 | }) 105 | }) 106 | 107 | t.Run("pkg/errors.WithMessage", func(t *testing.T) { 108 | err0 := errors.New("error") 109 | err1 := pkgerrors.WithMessage(err0, "message") 110 | 111 | pkgErr := extractPkgError(err1) 112 | assert.NotNil(t, pkgErr) 113 | assert.Equal(t, []string{"message"}, pkgErr.Messages) 114 | assert.Equal(t, err0, pkgErr.Err) 115 | assert.Empty(t, pkgErr.StackTrace) 116 | }) 117 | } 118 | 119 | func TestConvertPkgError(t *testing.T) { 120 | t.Run("nil", func(t *testing.T) { 121 | failErr := convertPkgError(nil) 122 | assert.Nil(t, failErr) 123 | }) 124 | 125 | t.Run("not a pkg/errors", func(t *testing.T) { 126 | failErr := convertPkgError(errors.New("error")) 127 | assert.Nil(t, failErr) 128 | }) 129 | 130 | t.Run("wrap", func(t *testing.T) { 131 | err0 := errors.New("error") 132 | err1 := pkgErrorsWrap(err0, "message 1") 133 | err2 := pkgErrorsWrap(err1, "message 2") 134 | 135 | failErr := convertPkgError(err2) 136 | assert.NotNil(t, failErr) 137 | assert.Equal(t, []string{"message 2", "message 1"}, failErr.Messages) 138 | assert.Equal(t, err0, failErr.Err) 139 | assert.NotEmpty(t, failErr.StackTrace) 140 | assert.Equal(t, "pkgErrorsWrap", failErr.StackTrace[0].Func) 141 | }) 142 | 143 | t.Run("mixed (inner most)", func(t *testing.T) { 144 | err0 := New("error") 145 | err1 := Wrap(err0, WithMessage("message 1")) 146 | err2 := pkgErrorsWrap(err1, "message 2") 147 | 148 | failErr := convertPkgError(err2) 149 | assert.NotNil(t, failErr) 150 | assert.Equal(t, []string{"message 2", "message 1"}, failErr.Messages) 151 | assert.Equal(t, err0.Error(), failErr.Err.Error()) 152 | assert.NotEmpty(t, failErr.StackTrace) 153 | }) 154 | 155 | t.Run("mixed (middle)", func(t *testing.T) { 156 | err0 := errors.New("error") 157 | err1 := pkgErrorsWrap(err0, "message 1") 158 | err2 := wrapOrigin(err1) 159 | err3 := pkgErrorsWrap(err2, "message 2") 160 | 161 | failErr := convertPkgError(err3) 162 | assert.NotNil(t, failErr) 163 | assert.Equal(t, []string{"message 2", "message 1"}, failErr.Messages) 164 | assert.Equal(t, err0, failErr.Err) 165 | assert.NotEmpty(t, failErr.StackTrace) 166 | assert.Equal(t, "pkgErrorsWrap", failErr.StackTrace[0].Func) 167 | }) 168 | } 169 | 170 | func pkgErrorsNew(msg string) error { 171 | return pkgerrors.New(msg) 172 | } 173 | 174 | func pkgErrorsWrap(err error, msg string) error { 175 | return pkgerrors.Wrap(err, msg) 176 | } 177 | 178 | type errorSlice []error 179 | 180 | func (s errorSlice) Error() string { 181 | msg := make([]string, len(s)) 182 | for i, e := range s { 183 | msg[i] = e.Error() 184 | } 185 | return strings.Join(msg, ": ") 186 | } 187 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "runtime" 5 | "strings" 6 | ) 7 | 8 | const ( 9 | stackMaxSize = 32 10 | stackBaseOffset = 3 11 | ) 12 | 13 | // StackTrace is a stack of Frame from innermost to outermost 14 | type StackTrace []Frame 15 | 16 | // Frame represents a single frame of stack trace 17 | type Frame struct { 18 | Func string 19 | File string 20 | Line int64 21 | } 22 | 23 | // newFrameFromRuntimeFrame creates Frame from the specified runtime.Frame 24 | func newFrameFromRuntimeFrame(rf runtime.Frame) (f Frame, ok bool) { 25 | f.Func = funcname(rf.Function) 26 | f.File = trimGOPATH(rf.Function, rf.File) 27 | f.Line = int64(rf.Line) 28 | 29 | if strings.HasPrefix(f.File, "runtime/") { 30 | return 31 | } 32 | 33 | ok = true 34 | return 35 | } 36 | 37 | // newStackTrace creates StackTrace by callers 38 | func newStackTrace(offset int) StackTrace { 39 | pcs := make([]uintptr, stackMaxSize) 40 | n := runtime.Callers(stackBaseOffset+offset, pcs[:]) 41 | return newStackTraceFromPCs(pcs[:n]) 42 | } 43 | 44 | // newStackTraceFromPCs creates StackTrace from program counters 45 | func newStackTraceFromPCs(pcs []uintptr) (frames StackTrace) { 46 | runtimeFrames := runtime.CallersFrames(pcs) 47 | 48 | for { 49 | rf, more := runtimeFrames.Next() 50 | if frame, ok := newFrameFromRuntimeFrame(rf); ok { 51 | frames = append(frames, frame) 52 | } 53 | if !more { 54 | break 55 | } 56 | } 57 | 58 | return frames 59 | } 60 | 61 | // funcname removes the path prefix component of a function's name reported by func.Name(). 62 | // Copied from https://github.com/pkg/errors/blob/master/stack.go 63 | func funcname(name string) string { 64 | i := strings.LastIndex(name, "/") 65 | name = name[i+1:] 66 | i = strings.Index(name, ".") 67 | return name[i+1:] 68 | } 69 | 70 | // Copied from https://github.com/pkg/errors/blob/master/stack.go 71 | func trimGOPATH(name, file string) string { 72 | // Here we want to get the source file path relative to the compile time 73 | // GOPATH. As of Go 1.6.x there is no direct way to know the compiled 74 | // GOPATH at runtime, but we can infer the number of path segments in the 75 | // GOPATH. We note that fn.Name() returns the function name qualified by 76 | // the import path, which does not include the GOPATH. Thus we can trim 77 | // segments from the beginning of the file path until the number of path 78 | // separators remaining is one more than the number of path separators in 79 | // the function name. For example, given: 80 | // 81 | // GOPATH /home/user 82 | // file /home/user/src/pkg/sub/file.go 83 | // fn.Name() pkg/sub.Type.Method 84 | // 85 | // We want to produce: 86 | // 87 | // pkg/sub/file.go 88 | // 89 | // From this we can easily see that fn.Name() has one less path separator 90 | // than our desired output. We count separators from the end of the file 91 | // path until it finds two more than in the function name and then move 92 | // one character forward to preserve the initial path segment without a 93 | // leading separator. 94 | const sep = "/" 95 | goal := strings.Count(name, sep) + 2 96 | i := len(file) 97 | for n := 0; n < goal; n++ { 98 | i = strings.LastIndex(file[:i], sep) 99 | if i == -1 { 100 | // not enough separators found, set i so that the slice expression 101 | // below leaves file unmodified 102 | i = -len(sep) 103 | break 104 | } 105 | } 106 | // get back to 0 or trim the leading separator 107 | file = file[i+len(sep):] 108 | return file 109 | } 110 | 111 | // mergeStackTraces merges two stack traces 112 | func mergeStackTraces(inner StackTrace, outer StackTrace) StackTrace { 113 | innerLen := len(inner) 114 | outerLen := len(outer) 115 | 116 | if innerLen > outerLen { 117 | overlap := 0 118 | for overlap < outerLen { 119 | if inner[innerLen-overlap-1] != outer[outerLen-overlap-1] { 120 | break 121 | } 122 | overlap++ 123 | } 124 | 125 | if overlap > 0 { 126 | return append(inner[:innerLen-overlap], outer...) 127 | } 128 | } 129 | 130 | return append(inner, outer...) 131 | } 132 | 133 | // reduceStackTraces incrementally merges multiple stack traces 134 | // and returns a merged stack trace 135 | func reduceStackTraces(stackTraces []StackTrace) (merged StackTrace) { 136 | for i := len(stackTraces) - 1; i >= 0; i-- { 137 | merged = mergeStackTraces(merged, stackTraces[i]) 138 | } 139 | return 140 | } 141 | -------------------------------------------------------------------------------- /stack_test.go: -------------------------------------------------------------------------------- 1 | package fail 2 | 3 | import ( 4 | "regexp" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestNewStackTrace(t *testing.T) { 11 | var st0 StackTrace 12 | func() { 13 | st0 = newStackTrace(0) 14 | }() 15 | 16 | var st1 StackTrace 17 | func() { 18 | func() { 19 | st1 = newStackTrace(1) 20 | }() 21 | }() 22 | 23 | t.Run("offset 0", func(t *testing.T) { 24 | assert.NotEmpty(t, st0) 25 | assert.Equal(t, "TestNewStackTrace", st0[0].Func) 26 | assert.Regexp(t, regexp.MustCompile(`github.com/\w+/fail/stack_test.go`), st0[0].File) 27 | assert.NotZero(t, st0[0].Line) 28 | }) 29 | 30 | t.Run("offset n", func(t *testing.T) { 31 | assert.NotEmpty(t, st1) 32 | assert.Equal(t, "TestNewStackTrace", st1[0].Func) 33 | assert.Regexp(t, regexp.MustCompile(`github.com/\w+/fail/stack_test.go`), st1[0].File) 34 | assert.NotZero(t, st1[0].Line) 35 | }) 36 | } 37 | 38 | func TestFuncname(t *testing.T) { 39 | tests := map[string]string{ 40 | "": "", 41 | "runtime.main": "main", 42 | "github.com/srvc/fail.funcname": "funcname", 43 | "funcname": "funcname", 44 | "io.copyBuffer": "copyBuffer", 45 | "main.(*R).Write": "(*R).Write", 46 | } 47 | 48 | for input, expect := range tests { 49 | assert.Equal(t, expect, funcname(input)) 50 | } 51 | } 52 | 53 | func TestTrimGOPATH(t *testing.T) { 54 | gopath := "/home/user" 55 | file := gopath + "/src/pkg/sub/file.go" 56 | funcName := "pkg/sub.Type.Method" 57 | 58 | assert.Equal(t, "pkg/sub/file.go", trimGOPATH(funcName, file)) 59 | } 60 | 61 | func TestMergeStackTraces(t *testing.T) { 62 | t.Run("empty", func(t *testing.T) { 63 | inner := StackTrace{} 64 | outer := StackTrace{ 65 | {Func: "init", File: "main.go", Line: 154}, 66 | } 67 | result := StackTrace{ 68 | {Func: "init", File: "main.go", Line: 154}, 69 | } 70 | 71 | assert.Equal(t, result, mergeStackTraces(inner, outer)) 72 | }) 73 | 74 | t.Run("inner < outer", func(t *testing.T) { 75 | inner := StackTrace{ 76 | {Func: "init", File: "main.go", Line: 154}, 77 | } 78 | outer := StackTrace{ 79 | {Func: "f1", File: "main.go", Line: 157}, 80 | {Func: "f2", File: "main.go", Line: 161}, 81 | {Func: "f3.func1", File: "main.go", Line: 167}, 82 | } 83 | result := StackTrace{ 84 | {Func: "init", File: "main.go", Line: 154}, 85 | {Func: "f1", File: "main.go", Line: 157}, 86 | {Func: "f2", File: "main.go", Line: 161}, 87 | {Func: "f3.func1", File: "main.go", Line: 167}, 88 | } 89 | 90 | assert.Equal(t, result, mergeStackTraces(inner, outer)) 91 | }) 92 | 93 | t.Run("inner > outer (overlapping)", func(t *testing.T) { 94 | inner := StackTrace{ 95 | {Func: "init", File: "main.go", Line: 154}, 96 | {Func: "f1", File: "main.go", Line: 157}, 97 | {Func: "f2", File: "main.go", Line: 161}, 98 | {Func: "f3.func1", File: "main.go", Line: 167}, 99 | } 100 | outer := StackTrace{ 101 | {Func: "f2", File: "main.go", Line: 161}, 102 | {Func: "f3.func1", File: "main.go", Line: 167}, 103 | } 104 | result := StackTrace{ 105 | {Func: "init", File: "main.go", Line: 154}, 106 | {Func: "f1", File: "main.go", Line: 157}, 107 | {Func: "f2", File: "main.go", Line: 161}, 108 | {Func: "f3.func1", File: "main.go", Line: 167}, 109 | } 110 | 111 | assert.Equal(t, result, mergeStackTraces(inner, outer)) 112 | }) 113 | 114 | t.Run("inner > outer (no overlapping frames)", func(t *testing.T) { 115 | inner := StackTrace{ 116 | {Func: "init", File: "main.go", Line: 154}, 117 | {Func: "f1", File: "main.go", Line: 157}, 118 | {Func: "f2", File: "main.go", Line: 161}, 119 | {Func: "f3.func1", File: "main.go", Line: 167}, 120 | } 121 | outer := StackTrace{ 122 | {Func: "g2", File: "main.go", Line: 1061}, 123 | {Func: "g3.func1", File: "main.go", Line: 1067}, 124 | } 125 | result := StackTrace{ 126 | {Func: "init", File: "main.go", Line: 154}, 127 | {Func: "f1", File: "main.go", Line: 157}, 128 | {Func: "f2", File: "main.go", Line: 161}, 129 | {Func: "f3.func1", File: "main.go", Line: 167}, 130 | {Func: "g2", File: "main.go", Line: 1061}, 131 | {Func: "g3.func1", File: "main.go", Line: 1067}, 132 | } 133 | 134 | assert.Equal(t, result, mergeStackTraces(inner, outer)) 135 | }) 136 | } 137 | 138 | func TestReduceStackTraces(t *testing.T) { 139 | input := []StackTrace{ 140 | { 141 | {Func: "main", File: "main.go", Line: 179}, 142 | }, 143 | { 144 | {Func: "f3.func1", File: "main.go", Line: 168}, 145 | }, 146 | { 147 | {Func: "f2", File: "main.go", Line: 162}, 148 | {Func: "f3.func1", File: "main.go", Line: 168}, 149 | }, 150 | { 151 | {Func: "f1", File: "main.go", Line: 158}, 152 | {Func: "f2", File: "main.go", Line: 162}, 153 | {Func: "f3.func1", File: "main.go", Line: 168}, 154 | }, 155 | { 156 | {Func: "init", File: "main.go", Line: 155}, 157 | }, 158 | {}, 159 | } 160 | result := StackTrace{ 161 | {Func: "init", File: "main.go", Line: 155}, 162 | {Func: "f1", File: "main.go", Line: 158}, 163 | {Func: "f2", File: "main.go", Line: 162}, 164 | {Func: "f3.func1", File: "main.go", Line: 168}, 165 | {Func: "main", File: "main.go", Line: 179}, 166 | } 167 | 168 | assert.Equal(t, result, reduceStackTraces(input)) 169 | } 170 | --------------------------------------------------------------------------------