├── .travis.yml ├── LICENSE.txt ├── README.md ├── errorx.go └── errorx_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.4 5 | script: 6 | - go test 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Maciej Lisiewski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/goware/errorx.svg?branch=master)](https://travis-ci.org/goware/errorx) 2 | [![GoDoc](https://godoc.org/github.com/goware/errorx?status.svg)](https://godoc.org/github.com/goware/errorx) 3 | 4 | # errorx 5 | Feature-rich Golang error interface implementation inspired by Postgres error message style guide http://www.postgresql.org/docs/devel/static/error-style-guide.html 6 | 7 | # features 8 | * Error codes 9 | * Verbosity levels 10 | * **File and line on which the error occures** (Debug+ verbosity level). Not 100% accurate, but close enough: shows file/line where errorx is rendered to string/JSON 11 | * error Stack traces (on verbosity level Trace) 12 | * Nested errors (both regular Golang `error` and `Errorx`) 13 | * Everything Golang `error` has - it's a drop-in replacement, because it implements `error` interface 14 | * Everything Golang `errors` package provides 15 | * JSON errors you can just write to your webhandler 16 | 17 | # docs 18 | http://godoc.org/github.com/goware/errorx 19 | 20 | # example output 21 | ### json, nested error, verbosity: Trace 22 | ```json 23 | { 24 | "error_code":10, 25 | "error_message":"error message", 26 | "error_details":[ 27 | "error details", 28 | "error hint" 29 | ], 30 | "cause":{ 31 | "error_code":200, 32 | "error_message":"wrapped error message", 33 | "error_details":[ 34 | "wrapped error details", 35 | "wrapped error hint" 36 | ] 37 | }, 38 | "stack":[ 39 | { 40 | "file":"errorx_test.go", 41 | "line":175, 42 | "function":"github.com/goware/errorx_test.TestJsonErrorEmbedding" 43 | }, 44 | { 45 | "file":"testing.go", 46 | "line":447, 47 | "function":"testing.tRunner" 48 | }, 49 | { 50 | "file":"asm_amd64.s", 51 | "line":2232, 52 | "function":"runtime.goexit" 53 | } 54 | ] 55 | } 56 | ``` 57 | 58 | ### string (via .Error()), verbosity: Debug 59 | ``` 60 | errorx_test.go:28: error 10: error message | error details; error hint 61 | ``` 62 | -------------------------------------------------------------------------------- /errorx.go: -------------------------------------------------------------------------------- 1 | package errorx 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "path/filepath" 8 | "runtime" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | Info = iota 14 | Verbose 15 | Debug 16 | Trace 17 | ) 18 | 19 | // verbosity variable stores global verbosity setting for errorx package 20 | // based on it's value different level of error details will be provided 21 | // by Error, Errorf, Json and Jsonf 22 | var verbosity int 23 | 24 | // Errorx is a more feature rich implementation of error interface inspired 25 | // by PostgreSQL error style guide 26 | type Errorx struct { 27 | Code int `json:"error_code,omitempty"` 28 | Message string `json:"error_message"` 29 | Details []string `json:"error_details,omitempty"` 30 | Cause error `json:"cause,omitempty"` 31 | Stack Stack `json:"stack,omitempty"` 32 | } 33 | 34 | // StackFrame represents a single frame of a stack trace 35 | type StackFrame struct { 36 | File string `json:"file,omitempty"` 37 | Line int `json:"line,omitempty"` 38 | Function string `json:"function,omitempty"` 39 | } 40 | 41 | // Stack is a slice of StackFrames representing a stack trace 42 | type Stack []StackFrame 43 | 44 | // String generates a string representation of a stack trace generated by 45 | // getTrace 46 | func (s Stack) String() string { 47 | var buffer bytes.Buffer 48 | for i := 0; i < len(s); i++ { 49 | if s[i].File == "" { 50 | break 51 | } 52 | buffer.WriteString(fmt.Sprintf("\n%s:%d %s", s[i].File, s[i].Line, s[i].Function)) 53 | } 54 | return buffer.String() 55 | } 56 | 57 | // New returns an error with error code and error messages provided in 58 | // function params 59 | func New(code int, ErrorMsg ...string) *Errorx { 60 | e := Errorx{Code: code} 61 | 62 | msgCount := len(ErrorMsg) 63 | if msgCount > 0 { 64 | e.Message = ErrorMsg[0] 65 | } 66 | if msgCount > 1 { 67 | e.Details = ErrorMsg[1:] 68 | } 69 | 70 | return &e 71 | } 72 | 73 | // Wrap wraps error in Errorx for nested errors 74 | func (e *Errorx) Wrap(err error) { 75 | e.Cause = err 76 | } 77 | 78 | // SetVerbosity changes global verbosity setting 79 | func SetVerbosity(v int) { 80 | verbosity = v 81 | } 82 | 83 | // ErrorCode returns Errorx error code value. It's intended primarily to allow 84 | // easy error comparison / matching 85 | func (e Errorx) ErrorCode() int { 86 | return e.Code 87 | } 88 | 89 | // Error returns a string representation of errorx. It includes at least 90 | // error code and message. Error details and hint are provided depending 91 | // on verbosity level set 92 | func (e Errorx) Error() string { 93 | maxMsg := len(e.Details) 94 | if maxMsg > verbosity { 95 | maxMsg = verbosity 96 | } 97 | 98 | switch verbosity { 99 | case 0: 100 | return fmt.Sprintf("error %d: %s", e.Code, e.Message) 101 | case 1: 102 | if e.Cause == nil { 103 | return fmt.Sprintf("error %d: %s | %s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; ")) 104 | } 105 | return fmt.Sprintf("error %d: %s | %s\ncause: %s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Cause.Error()) 106 | case 2: 107 | e.getTrace() 108 | 109 | if e.Cause == nil { 110 | if e.Stack[0].File != "" { 111 | return fmt.Sprintf("%s:%d: error %d: %s | %s", e.Stack[0].File, e.Stack[0].Line, e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; ")) 112 | } 113 | return fmt.Sprintf("error %d: %s | %s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; ")) 114 | } 115 | if e.Stack[0].File != "" { 116 | return fmt.Sprintf("%s:%d: error %d: %s | %s\ncause: %s", e.Stack[0].File, e.Stack[0].Line, e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Cause.Error()) 117 | } 118 | return fmt.Sprintf("error %d: %s | %s\ncause: %s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Cause.Error()) 119 | default: 120 | e.getTrace() 121 | 122 | if e.Cause == nil { 123 | if e.Stack[0].File != "" { 124 | return fmt.Sprintf("%s:%d: error %d: %s | %s%s", e.Stack[0].File, e.Stack[0].Line, e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Stack.String()) 125 | } 126 | return fmt.Sprintf("error %d: %s | %s%s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Stack.String()) 127 | } 128 | if e.Stack[0].File != "" { 129 | return fmt.Sprintf("%s:%d: error %d: %s | %s\ncause: %s%s", e.Stack[0].File, e.Stack[0].Line, e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Cause.Error(), e.Stack.String()) 130 | } 131 | return fmt.Sprintf("error %d: %s | %s\ncause: %s%s", e.Code, e.Message, strings.Join(e.Details[0:maxMsg], "; "), e.Cause.Error(), e.Stack.String()) 132 | } 133 | } 134 | 135 | // Json returns a json representation (as []byte) of errorx and error 136 | // if marshaling fails 137 | func (e Errorx) Json() ([]byte, error) { 138 | e.getTrace() 139 | err := e.verbositySubset() 140 | 141 | return json.Marshal(err) 142 | } 143 | 144 | // verbositySubset returns a subset of Errorx fields, depending on verbosity, 145 | // allowing only a part of the error to be marshaled to JSON 146 | func (e Errorx) verbositySubset() Errorx { 147 | err := Errorx{Code: e.Code, Message: e.Message} 148 | maxMsg := len(e.Details) 149 | if maxMsg > verbosity { 150 | maxMsg = verbosity 151 | } 152 | 153 | if verbosity > 0 { 154 | err.Details = e.Details[0:maxMsg] 155 | } 156 | if verbosity > 1 { 157 | if e.Cause != nil { 158 | if cause, ok := e.Cause.(*Errorx); ok { 159 | err.Cause = cause.verbositySubset() 160 | } else { 161 | err.Cause = Errorx{Message: e.Cause.Error()} 162 | } 163 | } 164 | err.Stack = e.Stack 165 | } 166 | return err 167 | } 168 | 169 | // getTrace generates trace for Errorx 170 | func (e *Errorx) getTrace() { 171 | if verbosity < 2 { 172 | return 173 | } 174 | 175 | if verbosity == 2 { 176 | pc, fn, line, ok := runtime.Caller(2) 177 | if !ok { 178 | return 179 | } 180 | 181 | s := StackFrame{Line: line} 182 | s.Function = funcName(pc) 183 | _, s.File = filepath.Split(fn) 184 | 185 | e.Stack = []StackFrame{s} 186 | return 187 | } 188 | 189 | e.Stack = make([]StackFrame, 0) 190 | 191 | for i := 2; ; i++ { 192 | pc, fn, line, ok := runtime.Caller(i) 193 | if !ok { 194 | // no more frames - we're done 195 | break 196 | } 197 | _, fn = filepath.Split(fn) 198 | 199 | f := StackFrame{File: fn, Line: line, Function: funcName(pc)} 200 | e.Stack = append(e.Stack, f) 201 | } 202 | } 203 | 204 | // funcName gets the name of the function at pointer or "??" if one can't be found 205 | func funcName(pc uintptr) string { 206 | if f := runtime.FuncForPC(pc); f != nil { 207 | return f.Name() 208 | } 209 | return "??" 210 | } 211 | -------------------------------------------------------------------------------- /errorx_test.go: -------------------------------------------------------------------------------- 1 | package errorx_test 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | 7 | "github.com/goware/errorx" 8 | ) 9 | 10 | func TestErrorVerbosity(t *testing.T) { 11 | e := errorx.New(10, "error message", "error details", "error hint") 12 | 13 | errorx.SetVerbosity(errorx.Info) 14 | err := e.Error() 15 | expected := "error 10: error message" 16 | if err != expected { 17 | t.Errorf("Expected %s, got '%s'", expected, err) 18 | } 19 | 20 | errorx.SetVerbosity(errorx.Verbose) 21 | err = e.Error() 22 | expected = "error 10: error message | error details" 23 | if err != expected { 24 | t.Errorf("Expected %s, got '%s'", expected, err) 25 | } 26 | 27 | errorx.SetVerbosity(errorx.Debug) 28 | err = e.Error() 29 | expected = "errorx_test.go:28: error 10: error message | error details; error hint" 30 | if err != expected { 31 | t.Errorf("Expected %s, got '%s'", expected, err) 32 | } 33 | 34 | errorx.SetVerbosity(errorx.Trace) 35 | err = e.Error() 36 | expected = "errorx_test.go:35: error 10: error message | error details; error hint\nerrorx_test.go:35 github.com/goware/errorx_test.TestErrorVerbosity\ntesting.go:447 testing.tRunner\nasm_amd64.s:2232 runtime.goexit" 37 | if err != expected { 38 | t.Errorf("Expected %s, got '%s'", expected, err) 39 | } 40 | } 41 | 42 | func TestJsonVerbosity(t *testing.T) { 43 | e := errorx.New(12, "error message", "error details", "error hint") 44 | 45 | errorx.SetVerbosity(errorx.Info) 46 | err, _ := e.Json() 47 | expected := `{"error_code":12,"error_message":"error message"}` 48 | if string(err) != expected { 49 | t.Errorf(`Expected '%s', got '%s'`, expected, string(err)) 50 | } 51 | 52 | errorx.SetVerbosity(errorx.Verbose) 53 | err, _ = e.Json() 54 | expected = `{"error_code":12,"error_message":"error message","error_details":["error details"]}` 55 | if string(err) != expected { 56 | t.Errorf(`Expected '%s', got '%s'`, expected, string(err)) 57 | } 58 | 59 | errorx.SetVerbosity(errorx.Debug) 60 | err, _ = e.Json() 61 | expected = `{"error_code":12,"error_message":"error message","error_details":["error details","error hint"],"stack":[{"file":"errorx_test.go","line":60,"function":"github.com/goware/errorx_test.TestJsonVerbosity"}]}` 62 | if string(err) != expected { 63 | t.Errorf(`Expected '%s', got '%s'`, expected, string(err)) 64 | } 65 | } 66 | 67 | func TestErrorCode(t *testing.T) { 68 | e := errorx.New(14, "error message", "error details", "error hint") 69 | 70 | if e.ErrorCode() != 14 { 71 | t.Errorf(`Invalide error code - expected 14, got %d`, e.ErrorCode()) 72 | } 73 | } 74 | 75 | func TestErrorEmbedding(t *testing.T) { 76 | wrappableErrorx := errorx.New(200, "wrapped error message", "wrapped error details", "wrapped error hint") 77 | wrappableError := errors.New("wrapped regular error") 78 | e1 := errorx.New(10, "error message", "error details", "error hint") 79 | e1.Wrap(wrappableErrorx) 80 | e2 := errorx.New(11, "error message", "error details", "error hint") 81 | e2.Wrap(wrappableError) 82 | 83 | errorx.SetVerbosity(errorx.Info) 84 | expected := "error 10: error message" 85 | if e1.Error() != expected { 86 | t.Errorf("Expected '%s', got '%s'", expected, e1.Error()) 87 | } 88 | expected = "error 11: error message" 89 | if e2.Error() != expected { 90 | t.Errorf("Expected '%s', got '%s'", expected, e2.Error()) 91 | } 92 | 93 | errorx.SetVerbosity(errorx.Verbose) 94 | expected = "error 10: error message | error details\ncause: error 200: wrapped error message | wrapped error details" 95 | if e1.Error() != expected { 96 | t.Errorf("Expected '%s', got '%s'", expected, e1.Error()) 97 | } 98 | expected = "error 11: error message | error details\ncause: wrapped regular error" 99 | if e2.Error() != expected { 100 | t.Errorf("Expected '%s', got '%s'", expected, e2.Error()) 101 | } 102 | 103 | errorx.SetVerbosity(errorx.Debug) 104 | err := e1.Error() 105 | expected = "errorx_test.go:104: error 10: error message | error details; error hint\ncause: error 200: wrapped error message | wrapped error details; wrapped error hint" 106 | if err != expected { 107 | t.Errorf("Expected '%s', got '%s'", expected, err) 108 | } 109 | 110 | err = e2.Error() 111 | expected = "errorx_test.go:110: error 11: error message | error details; error hint\ncause: wrapped regular error" 112 | if err != expected { 113 | t.Errorf("Expected '%s', got '%s'", expected, err) 114 | } 115 | 116 | errorx.SetVerbosity(errorx.Trace) 117 | err = e1.Error() 118 | expected = "errorx_test.go:117: error 10: error message | error details; error hint\ncause: error 200: wrapped error message | wrapped error details; wrapped error hint\nerrorx_test.go:117 github.com/goware/errorx_test.TestErrorEmbedding\ntesting.go:447 testing.tRunner\nasm_amd64.s:2232 runtime.goexit" 119 | if err != expected { 120 | t.Errorf("Expected '%s', got '%s'", expected, err) 121 | } 122 | 123 | err = e2.Error() 124 | expected = "errorx_test.go:123: error 11: error message | error details; error hint\ncause: wrapped regular error\nerrorx_test.go:123 github.com/goware/errorx_test.TestErrorEmbedding\ntesting.go:447 testing.tRunner\nasm_amd64.s:2232 runtime.goexit" 125 | if err != expected { 126 | t.Errorf("Expected '%s', got '%s'", expected, err) 127 | } 128 | } 129 | 130 | func TestJsonErrorEmbedding(t *testing.T) { 131 | wrappableErrorx := errorx.New(200, "wrapped error message", "wrapped error details", "wrapped error hint") 132 | wrappableError := errors.New("wrapped regular error") 133 | e1 := errorx.New(10, "error message", "error details", "error hint") 134 | e1.Wrap(wrappableErrorx) 135 | e2 := errorx.New(11, "error message", "error details", "error hint") 136 | e2.Wrap(wrappableError) 137 | 138 | errorx.SetVerbosity(errorx.Info) 139 | e, _ := e1.Json() 140 | expected := `{"error_code":10,"error_message":"error message"}` 141 | if string(e) != expected { 142 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 143 | } 144 | e, _ = e2.Json() 145 | expected = `{"error_code":11,"error_message":"error message"}` 146 | if string(e) != expected { 147 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 148 | } 149 | 150 | errorx.SetVerbosity(errorx.Verbose) 151 | e, _ = e1.Json() 152 | expected = `{"error_code":10,"error_message":"error message","error_details":["error details"]}` 153 | if string(e) != expected { 154 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 155 | } 156 | e, _ = e2.Json() 157 | expected = `{"error_code":11,"error_message":"error message","error_details":["error details"]}` 158 | if string(e) != expected { 159 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 160 | } 161 | 162 | errorx.SetVerbosity(errorx.Debug) 163 | e, _ = e1.Json() 164 | expected = `{"error_code":10,"error_message":"error message","error_details":["error details","error hint"],"cause":{"error_code":200,"error_message":"wrapped error message","error_details":["wrapped error details","wrapped error hint"]},"stack":[{"file":"errorx_test.go","line":163,"function":"github.com/goware/errorx_test.TestJsonErrorEmbedding"}]}` 165 | if string(e) != expected { 166 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 167 | } 168 | e, _ = e2.Json() 169 | expected = `{"error_code":11,"error_message":"error message","error_details":["error details","error hint"],"cause":{"error_message":"wrapped regular error"},"stack":[{"file":"errorx_test.go","line":168,"function":"github.com/goware/errorx_test.TestJsonErrorEmbedding"}]}` 170 | if string(e) != expected { 171 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 172 | } 173 | 174 | errorx.SetVerbosity(errorx.Trace) 175 | e, _ = e1.Json() 176 | expected = `{"error_code":10,"error_message":"error message","error_details":["error details","error hint"],"cause":{"error_code":200,"error_message":"wrapped error message","error_details":["wrapped error details","wrapped error hint"]},"stack":[{"file":"errorx_test.go","line":175,"function":"github.com/goware/errorx_test.TestJsonErrorEmbedding"},{"file":"testing.go","line":447,"function":"testing.tRunner"},{"file":"asm_amd64.s","line":2232,"function":"runtime.goexit"}]}` 177 | if string(e) != expected { 178 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 179 | } 180 | e, _ = e2.Json() 181 | expected = `{"error_code":11,"error_message":"error message","error_details":["error details","error hint"],"cause":{"error_message":"wrapped regular error"},"stack":[{"file":"errorx_test.go","line":180,"function":"github.com/goware/errorx_test.TestJsonErrorEmbedding"},{"file":"testing.go","line":447,"function":"testing.tRunner"},{"file":"asm_amd64.s","line":2232,"function":"runtime.goexit"}]}` 182 | if string(e) != expected { 183 | t.Errorf("Expected '%s', got '%s'", expected, string(e)) 184 | } 185 | } 186 | --------------------------------------------------------------------------------