├── .github └── workflows │ ├── gosec.yml │ ├── test.yml │ └── trivy.yml ├── LICENSE ├── README.md ├── builder.go ├── builder_test.go ├── errors.go ├── errors_test.go ├── examples ├── basic │ └── main.go ├── builder │ └── main.go ├── errors_is │ └── main.go ├── logging │ └── main.go ├── stacktrace_extract │ └── main.go ├── stacktrace_print │ └── main.go ├── tag │ └── main.go └── variables │ └── main.go ├── go.mod ├── go.sum ├── stack.go ├── tag.go └── tag_test.go /.github/workflows/gosec.yml: -------------------------------------------------------------------------------- 1 | name: "gosec" 2 | 3 | # Run workflow each time code is pushed to your repository and on a schedule. 4 | # The scheduled workflow runs every at 00:00 on Sunday UTC time. 5 | on: 6 | push: 7 | 8 | jobs: 9 | tests: 10 | runs-on: ubuntu-latest 11 | env: 12 | GO111MODULE: on 13 | steps: 14 | - name: Checkout Source 15 | uses: actions/checkout@v2 16 | - name: Run Gosec Security Scanner 17 | uses: securego/gosec@master 18 | with: 19 | # we let the report trigger content trigger a failure using the GitHub Security features. 20 | args: "-no-fail -fmt sarif -out results.sarif ./..." 21 | - name: Upload SARIF file 22 | uses: github/codeql-action/upload-sarif@v1 23 | with: 24 | # Path to SARIF file relative to the root of the repository 25 | sarif_file: results.sarif 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push] 4 | 5 | jobs: 6 | testing: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout upstream repo 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | - uses: actions/setup-go@v4 15 | with: 16 | go-version-file: "go.mod" 17 | - run: go test . 18 | - run: go vet . 19 | 20 | -------------------------------------------------------------------------------- /.github/workflows/trivy.yml: -------------------------------------------------------------------------------- 1 | name: package scan 2 | 3 | on: [push] 4 | 5 | jobs: 6 | scan: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Checkout upstream repo 11 | uses: actions/checkout@v2 12 | with: 13 | ref: ${{ github.head_ref }} 14 | - name: Run Trivy vulnerability scanner in repo mode 15 | uses: aquasecurity/trivy-action@master 16 | with: 17 | scan-type: "fs" 18 | ignore-unfixed: true 19 | format: "template" 20 | template: "@/contrib/sarif.tpl" 21 | output: "trivy-results.sarif" 22 | skip-dirs: pkg/infra/trivy/testdata 23 | 24 | - name: Upload Trivy scan results to GitHub Security tab 25 | uses: github/codeql-action/upload-sarif@v1 26 | with: 27 | sarif_file: "trivy-results.sarif" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021, Masayoshi Mizutani 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # goerr [![test](https://github.com/m-mizutani/goerr/actions/workflows/test.yml/badge.svg)](https://github.com/m-mizutani/goerr/actions/workflows/test.yml) [![gosec](https://github.com/m-mizutani/goerr/actions/workflows/gosec.yml/badge.svg)](https://github.com/m-mizutani/goerr/actions/workflows/gosec.yml) [![package scan](https://github.com/m-mizutani/goerr/actions/workflows/trivy.yml/badge.svg)](https://github.com/m-mizutani/goerr/actions/workflows/trivy.yml) [![Go Reference](https://pkg.go.dev/badge/github.com/m-mizutani/goerr.svg)](https://pkg.go.dev/github.com/m-mizutani/goerr) 2 | 3 | Package `goerr` provides more contextual error handling in Go. 4 | 5 | ## Features 6 | 7 | `goerr` provides the following features: 8 | 9 | - Stack traces 10 | - Compatible with `github.com/pkg/errors`. 11 | - Structured stack traces with `goerr.Stack` is available. 12 | - Contextual variables to errors using: 13 | - Key value data by `goerr.Value(key, value)` (or `goerr.V(key, value)` as alias). 14 | - Tag value data can be defined by `goerr.NewTag` and set into error by `goerr.Tag(tag)` (or `goerr.T(tag)` as alias). 15 | - `errors.Is` to identify errors and `errors.As` to unwrap errors. 16 | - `slog.LogValuer` interface to output structured logs with `slog`. 17 | 18 | ## Usage 19 | 20 | You can install `goerr` by `go get`: 21 | 22 | ```sh 23 | go get github.com/m-mizutani/goerr/v2 24 | ``` 25 | 26 | ### Stack trace 27 | 28 | `goerr` records stack trace when creating an error. The format is compatible with `github.com/pkg/errors` and it can be used for [sentry.io](https://sentry.io), etc. 29 | 30 | ```go 31 | func someAction(fname string) error { 32 | if _, err := os.Open(fname); err != nil { 33 | return goerr.Wrap(err, "failed to open file") 34 | } 35 | return nil 36 | } 37 | 38 | func main() { 39 | if err := someAction("no_such_file.txt"); err != nil { 40 | log.Fatalf("%+v", err) 41 | } 42 | } 43 | ``` 44 | 45 | Output: 46 | ``` 47 | 2024/04/06 10:30:27 failed to open file: open no_such_file.txt: no such file or directory 48 | main.someAction 49 | /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:12 50 | main.main 51 | /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_print/main.go:18 52 | runtime.main 53 | /usr/local/go/src/runtime/proc.go:271 54 | runtime.goexit 55 | /usr/local/go/src/runtime/asm_arm64.s:1222 56 | exit status 1 57 | ``` 58 | 59 | You can not only print the stack trace, but also extract the stack trace by `goerr.Unwrap(err).Stacks()`. 60 | 61 | ```go 62 | if err := someAction("no_such_file.txt"); err != nil { 63 | // NOTE: `errors.Unwrap` also works 64 | if goErr := goerr.Unwrap(err); goErr != nil { 65 | for i, st := range goErr.Stacks() { 66 | log.Printf("%d: %v\n", i, st) 67 | } 68 | } 69 | log.Fatal(err) 70 | } 71 | ``` 72 | 73 | `Stacks()` returns a slice of `goerr.Stack` struct, which contains `Func`, `File`, and `Line`. 74 | 75 | ``` 76 | 2024/04/06 10:35:30 0: &{main.someAction /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 12} 77 | 2024/04/06 10:35:30 1: &{main.main /Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/stacktrace_extract/main.go 18} 78 | 2024/04/06 10:35:30 2: &{runtime.main /usr/local/go/src/runtime/proc.go 271} 79 | 2024/04/06 10:35:30 3: &{runtime.goexit /usr/local/go/src/runtime/asm_arm64.s 1222} 80 | 2024/04/06 10:35:30 failed to open file: open no_such_file.txt: no such file or directory 81 | exit status 1 82 | ``` 83 | 84 | **NOTE**: If the error is wrapped by `goerr` multiply, `%+v` will print the stack trace of the deepest error. 85 | 86 | **Tips**: If you want not to print the stack trace for current stack frame, you can use `Unstack` method. Also, `UnstackN` method removes the top multiple stack frames. 87 | 88 | ```go 89 | if err := someAction("no_such_file.txt"); err != nil { 90 | // Unstack() removes the current stack frame from the error message. 91 | return goerr.Wrap(err, "failed to someAction").Unstack() 92 | } 93 | ``` 94 | 95 | ### Add/Extract contextual variables 96 | 97 | `goerr` provides the `Value(key, value)` method to add contextual variables to errors. The standard way to handle errors in Go is by injecting values into error messages. However, this approach makes it difficult to aggregate various errors. On the other hand, `goerr`'s `Value` method allows for adding contextual information to errors without changing error message, making it easier to aggregate error logs. Additionally, error handling services like Sentry.io can handle errors more accurately with this feature. 98 | 99 | ```go 100 | var errFormatMismatch = errors.New("format mismatch") 101 | 102 | func someAction(tasks []task) error { 103 | for _, t := range tasks { 104 | if err := validateData(t.Data); err != nil { 105 | return goerr.Wrap(err, "failed to validate data", goerr.Value("name", t.Name)) 106 | } 107 | } 108 | // .... 109 | return nil 110 | } 111 | 112 | func validateData(data string) error { 113 | if !strings.HasPrefix(data, "data:") { 114 | return goerr.Wrap(errFormatMismatch, goerr.Value("data", data)) 115 | } 116 | return nil 117 | } 118 | 119 | type task struct { 120 | Name string 121 | Data string 122 | } 123 | 124 | func main() { 125 | tasks := []task{ 126 | {Name: "task1", Data: "data:1"}, 127 | {Name: "task2", Data: "invalid"}, 128 | {Name: "task3", Data: "data:3"}, 129 | } 130 | if err := someAction(tasks); err != nil { 131 | if goErr := goerr.Unwrap(err); goErr != nil { 132 | for k, v := range goErr.Values() { 133 | log.Printf("var: %s => %v\n", k, v) 134 | } 135 | } 136 | log.Fatalf("msg: %s", err) 137 | } 138 | } 139 | ``` 140 | 141 | Output: 142 | ``` 143 | 2024/04/06 14:40:59 var: data => invalid 144 | 2024/04/06 14:40:59 var: name => task2 145 | 2024/04/06 14:40:59 msg: failed to validate data: : format mismatch 146 | exit status 1 147 | ``` 148 | 149 | If you want to send the error to sentry.io with [SDK](https://docs.sentry.io/platforms/go/), you can extract the contextual variables by `goErr.Values()` and set them to the scope. 150 | 151 | ```go 152 | // Sending error to Sentry 153 | hub := sentry.CurrentHub().Clone() 154 | hub.ConfigureScope(func(scope *sentry.Scope) { 155 | if goErr := goerr.Unwrap(err); goErr != nil { 156 | for k, v := range goErr.Values() { 157 | scope.SetExtra(k, v) 158 | } 159 | } 160 | }) 161 | evID := hub.CaptureException(err) 162 | ``` 163 | 164 | #### Tags 165 | 166 | There are use cases where we need to adjust the error handling strategy based on the nature of the error. A clear example is an HTTP server, where the status code to be returned varies depending on whether it's an error from a downstream system, a missing resource, or an unauthorized request. To handle this precisely, you could predefine errors for each type and use methods like `errors.Is` in the error handling section to verify and branch the processing accordingly. However, this approach becomes challenging as the program grows larger and the number and variety of errors increase. 167 | 168 | `goerr` provides also `WithTags(tags ...string)` method to add tags to errors. Tags are useful when you want to categorize errors. For example, you can add tags like "critical" or "warning" to errors. 169 | 170 | ```go 171 | var ( 172 | ErrTagSysError = goerr.NewTag("system_error") 173 | ErrTagBadRequest = goerr.NewTag("bad_request") 174 | ) 175 | 176 | func handleError(w http.ResponseWriter, err error) { 177 | if goErr := goerr.Unwrap(err); goErr != nil { 178 | switch { 179 | case goErr.HasTag(ErrTagSysError): 180 | w.WriteHeader(http.StatusInternalServerError) 181 | case goErr.HasTag(ErrTagBadRequest): 182 | w.WriteHeader(http.StatusBadRequest) 183 | default: 184 | w.WriteHeader(http.StatusInternalServerError) 185 | } 186 | } else { 187 | w.WriteHeader(http.StatusInternalServerError) 188 | } 189 | _, _ = w.Write([]byte(err.Error())) 190 | } 191 | 192 | func someAction() error { 193 | if _, err := http.Get("http://example.com/some/resource"); err != nil { 194 | return goerr.Wrap(err, "failed to get some resource").WithTags(ErrTagSysError) 195 | } 196 | return nil 197 | } 198 | 199 | func main() { 200 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 201 | if err := someAction(); err != nil { 202 | handleError(w, err) 203 | return 204 | } 205 | w.WriteHeader(http.StatusOK) 206 | _, _ = w.Write([]byte("OK")) 207 | }) 208 | 209 | http.ListenAndServe(":8090", nil) 210 | } 211 | ``` 212 | 213 | ### Structured logging 214 | 215 | `goerr` provides `slog.LogValuer` interface to output structured logs with `slog`. It can be used to output not only the error message but also the stack trace and contextual variables. Additionally, unwrapped errors can be output recursively. 216 | 217 | ```go 218 | var errRuntime = errors.New("runtime error") 219 | 220 | func someAction(input string) error { 221 | if err := validate(input); err != nil { 222 | return goerr.Wrap(err, "failed validation") 223 | } 224 | return nil 225 | } 226 | 227 | func validate(input string) error { 228 | if input != "OK" { 229 | return goerr.Wrap(errRuntime, "invalid input", goerr.V("input", input)) 230 | } 231 | return nil 232 | } 233 | 234 | func main() { 235 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 236 | if err := someAction("ng"); err != nil { 237 | logger.Error("aborted myapp", slog.Any("error", err)) 238 | } 239 | } 240 | ``` 241 | 242 | Output: 243 | ```json 244 | { 245 | "time": "2024-04-06T11:32:40.350873+09:00", 246 | "level": "ERROR", 247 | "msg": "aborted myapp", 248 | "error": { 249 | "message": "failed validation", 250 | "stacktrace": [ 251 | "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:16 main.someAction", 252 | "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main", 253 | "/usr/local/go/src/runtime/proc.go:271 runtime.main", 254 | "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit" 255 | ], 256 | "cause": { 257 | "message": "invalid input", 258 | "values": { 259 | "input": "ng" 260 | }, 261 | "stacktrace": [ 262 | "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:23 main.validate", 263 | "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:15 main.someAction", 264 | "/Users/mizutani/.ghq/github.com/m-mizutani/goerr/examples/logging/main.go:30 main.main", 265 | "/usr/local/go/src/runtime/proc.go:271 runtime.main", 266 | "/usr/local/go/src/runtime/asm_arm64.s:1222 runtime.goexit" 267 | ], 268 | "cause": "runtime error" 269 | } 270 | } 271 | } 272 | ``` 273 | 274 | ### Builder 275 | 276 | `goerr` provides `goerr.NewBuilder()` to create an error with pre-defined contextual variables. It is useful when you want to create an error with the same contextual variables in multiple places. 277 | 278 | ```go 279 | type object struct { 280 | id string 281 | color string 282 | } 283 | 284 | func (o *object) Validate() error { 285 | eb := goerr.NewBuilder(goerr.Value("id", o.id)) 286 | 287 | if o.color == "" { 288 | return eb.New("color is empty") 289 | } 290 | 291 | return nil 292 | } 293 | 294 | func main() { 295 | obj := &object{id: "object-1"} 296 | 297 | if err := obj.Validate(); err != nil { 298 | slog.Default().Error("Validation error", "err", err) 299 | } 300 | } 301 | ``` 302 | 303 | Output: 304 | ``` 305 | 2024/10/19 14:19:54 ERROR Validation error err.message="color is empty" err.values.id=object-1 (snip) 306 | ``` 307 | 308 | ## License 309 | 310 | The 2-Clause BSD License. See [LICENSE](LICENSE) for more detail. 311 | -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | package goerr 2 | 3 | // Builder keeps a set of key-value pairs and can create a new error and wrap error with the key-value pairs. 4 | type Builder struct { 5 | options []Option 6 | } 7 | 8 | // NewBuilder creates a new Builder 9 | func NewBuilder(options ...Option) *Builder { 10 | return &Builder{ 11 | options: options, 12 | } 13 | } 14 | 15 | // With copies the current Builder and adds a new key-value pair. 16 | func (x *Builder) With(options ...Option) *Builder { 17 | newBuilder := &Builder{ 18 | options: x.options[:], 19 | } 20 | newBuilder.options = append(newBuilder.options, options...) 21 | return newBuilder 22 | } 23 | 24 | // New creates a new error with message 25 | func (x *Builder) New(msg string, options ...Option) *Error { 26 | err := newError(append(x.options, options...)...) 27 | err.msg = msg 28 | return err 29 | } 30 | 31 | // Wrap creates a new Error with caused error and add message. 32 | func (x *Builder) Wrap(cause error, msg string, options ...Option) *Error { 33 | err := newError(append(x.options, options...)...) 34 | err.msg = msg 35 | err.cause = cause 36 | return err 37 | } 38 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | package goerr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/m-mizutani/goerr/v2" 7 | ) 8 | 9 | func newErrorWithBuilder() *goerr.Error { 10 | return goerr.NewBuilder(goerr.V("color", "orange")).New("error") 11 | } 12 | 13 | func TestBuilderNew(t *testing.T) { 14 | err := newErrorWithBuilder() 15 | 16 | if err.Values()["color"] != "orange" { 17 | t.Errorf("Unexpected value: %v", err.Values()) 18 | } 19 | } 20 | 21 | func TestBuilderWrap(t *testing.T) { 22 | cause := goerr.New("cause") 23 | err := goerr.NewBuilder(goerr.V("color", "blue")).Wrap(cause, "error") 24 | 25 | if err.Values()["color"] != "blue" { 26 | t.Errorf("Unexpected value: %v", err.Values()) 27 | } 28 | 29 | if err.Unwrap().Error() != "cause" { 30 | t.Errorf("Unexpected cause: %v", err.Unwrap().Error()) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package goerr 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | 8 | "log/slog" 9 | 10 | "github.com/google/uuid" 11 | ) 12 | 13 | type Option func(*Error) 14 | 15 | // Value sets key and value to the error 16 | func Value(key string, value any) Option { 17 | return func(err *Error) { 18 | err.values[key] = value 19 | } 20 | } 21 | 22 | // V is alias of Value 23 | func V(key string, value any) Option { 24 | return Value(key, value) 25 | } 26 | 27 | // Tag sets tag to the error 28 | func Tag(t tag) Option { 29 | return func(err *Error) { 30 | err.tags[t] = struct{}{} 31 | } 32 | } 33 | 34 | // T is alias of Tag 35 | func T(t tag) Option { 36 | return Tag(t) 37 | } 38 | 39 | // New creates a new error with message 40 | func New(msg string, options ...Option) *Error { 41 | err := newError(options...) 42 | err.msg = msg 43 | return err 44 | } 45 | 46 | // Wrap creates a new Error and add message. 47 | func Wrap(cause error, msg string, options ...Option) *Error { 48 | err := newError(options...) 49 | err.msg = msg 50 | err.cause = cause 51 | 52 | return err 53 | } 54 | 55 | // Unwrap returns unwrapped goerr.Error from err by errors.As. If no goerr.Error, returns nil 56 | // NOTE: Do not receive error interface. It causes typed-nil problem. 57 | // 58 | // var err error = goerr.New("error") 59 | // if err != nil { // always true 60 | func Unwrap(err error) *Error { 61 | var e *Error 62 | if errors.As(err, &e) { 63 | return e 64 | } 65 | return nil 66 | } 67 | 68 | // Values returns map of key and value that is set by With. All wrapped goerr.Error key and values will be merged. Key and values of wrapped error is overwritten by upper goerr.Error. 69 | func Values(err error) map[string]any { 70 | if e := Unwrap(err); e != nil { 71 | return e.Values() 72 | } 73 | 74 | return nil 75 | } 76 | 77 | // Tags returns list of tags that is set by WithTags. All wrapped goerr.Error tags will be merged. Tags of wrapped error is overwritten by upper goerr.Error. 78 | func Tags(err error) []string { 79 | if e := Unwrap(err); e != nil { 80 | return e.Tags() 81 | } 82 | 83 | return nil 84 | } 85 | 86 | // HasTag returns true if the error has the tag. 87 | func HasTag(err error, tag tag) bool { 88 | if e := Unwrap(err); e != nil { 89 | return e.HasTag(tag) 90 | } 91 | 92 | return false 93 | } 94 | 95 | type values map[string]any 96 | 97 | func (x values) clone() values { 98 | newValues := make(values) 99 | for key, value := range x { 100 | newValues[key] = value 101 | } 102 | return newValues 103 | } 104 | 105 | // Error is error interface for deepalert to handle related variables 106 | type Error struct { 107 | msg string 108 | id string 109 | st *stack 110 | cause error 111 | values values 112 | tags tags 113 | } 114 | 115 | func newError(options ...Option) *Error { 116 | e := &Error{ 117 | st: callers(), 118 | values: make(values), 119 | id: uuid.New().String(), 120 | tags: make(tags), 121 | } 122 | 123 | for _, opt := range options { 124 | opt(e) 125 | } 126 | 127 | return e 128 | } 129 | 130 | func (x *Error) copy(dst *Error, options ...Option) { 131 | dst.msg = x.msg 132 | dst.id = x.id 133 | dst.cause = x.cause 134 | 135 | dst.tags = x.tags.clone() 136 | dst.values = x.values.clone() 137 | 138 | for _, opt := range options { 139 | opt(dst) 140 | } 141 | // st (stacktrace) is not copied 142 | } 143 | 144 | // Printable returns printable object 145 | func (x *Error) Printable() *Printable { 146 | e := &Printable{ 147 | Message: x.msg, 148 | ID: x.id, 149 | StackTrace: x.Stacks(), 150 | Values: make(map[string]any), 151 | } 152 | for k, v := range x.values { 153 | e.Values[k] = v 154 | } 155 | for tag := range x.tags { 156 | e.Tags = append(e.Tags, tag.value) 157 | } 158 | 159 | if cause := Unwrap(x.cause); cause != nil { 160 | e.Cause = cause.Printable() 161 | } else if x.cause != nil { 162 | e.Cause = x.cause.Error() 163 | } 164 | return e 165 | } 166 | 167 | type Printable struct { 168 | Message string `json:"message"` 169 | ID string `json:"id"` 170 | StackTrace []*Stack `json:"stacktrace"` 171 | Cause any `json:"cause"` 172 | Values map[string]any `json:"values"` 173 | Tags []string `json:"tags"` 174 | } 175 | 176 | // Error returns error message for error interface 177 | func (x *Error) Error() string { 178 | if x.cause == nil { 179 | return x.msg 180 | } 181 | 182 | return fmt.Sprintf("%s: %v", x.msg, x.cause.Error()) 183 | } 184 | 185 | // Format returns: 186 | // - %v, %s, %q: formatted message 187 | // - %+v: formatted message with stack trace 188 | func (x *Error) Format(s fmt.State, verb rune) { 189 | switch verb { 190 | case 'v': 191 | if s.Flag('+') { 192 | _, _ = io.WriteString(s, x.Error()) 193 | var c *Error 194 | for c = x; c.Unwrap() != nil; { 195 | cause, ok := c.Unwrap().(*Error) 196 | if !ok { 197 | break 198 | } 199 | c = cause 200 | } 201 | c.st.Format(s, verb) 202 | _, _ = io.WriteString(s, "\n") 203 | 204 | if len(x.values) > 0 { 205 | _, _ = io.WriteString(s, "\nValues:\n") 206 | for k, v := range x.values { 207 | _, _ = io.WriteString(s, fmt.Sprintf(" %s: %v\n", k, v)) 208 | } 209 | _, _ = io.WriteString(s, "\n") 210 | } 211 | return 212 | } 213 | fallthrough 214 | case 's': 215 | _, _ = io.WriteString(s, x.Error()) 216 | case 'q': 217 | fmt.Fprintf(s, "%q", x.Error()) 218 | } 219 | } 220 | 221 | // Unwrap returns *fundamental of github.com/pkg/errors 222 | func (x *Error) Unwrap() error { 223 | return x.cause 224 | } 225 | 226 | // Unstack trims stack trace by 1. It can be used for internal helper or utility functions. 227 | func (x *Error) Unstack() *Error { 228 | x.st = unstack(x.st, 1) 229 | return x 230 | } 231 | 232 | // UnstackN trims stack trace by n. It can be used for internal helper or utility functions. 233 | func (x *Error) UnstackN(n int) *Error { 234 | x.st = unstack(x.st, n) 235 | return x 236 | } 237 | 238 | // Is returns true if target is goerr.Error and Error.id of two errors are matched. It's for errors.Is. If Error.id is empty, it always returns false. 239 | func (x *Error) Is(target error) bool { 240 | var err *Error 241 | if errors.As(target, &err) { 242 | if x.id != "" && x.id == err.id { 243 | return true 244 | } 245 | } 246 | 247 | return x == target 248 | } 249 | 250 | // ID sets string to check equality in Error.IS() 251 | func (x *Error) ID(id string) *Error { 252 | x.id = id 253 | return x 254 | } 255 | 256 | // Wrap creates a new Error and copy message and id to new one. 257 | func (x *Error) Wrap(cause error, options ...Option) *Error { 258 | err := newError() 259 | x.copy(err, options...) 260 | err.cause = cause 261 | return err 262 | } 263 | 264 | // Values returns map of key and value that is set by With. All wrapped goerr.Error key and values will be merged. Key and values of wrapped error is overwritten by upper goerr.Error. 265 | func (x *Error) Values() map[string]any { 266 | values := x.mergedValues() 267 | 268 | for key, value := range x.values { 269 | values[key] = value 270 | } 271 | 272 | return values 273 | } 274 | 275 | func (x *Error) mergedValues() values { 276 | merged := make(values) 277 | 278 | if cause := x.Unwrap(); cause != nil { 279 | if err := Unwrap(cause); err != nil { 280 | merged = err.mergedValues() 281 | } 282 | } 283 | 284 | for key, value := range x.values { 285 | merged[key] = value 286 | } 287 | 288 | return merged 289 | } 290 | 291 | // Tags returns list of tags that is set by WithTags. All wrapped goerr.Error tags will be merged. Tags of wrapped error is overwritten by upper goerr.Error. 292 | func (x *Error) Tags() []string { 293 | tags := x.mergedTags() 294 | 295 | for tag := range x.tags { 296 | tags[tag] = struct{}{} 297 | } 298 | 299 | tagList := make([]string, 0, len(tags)) 300 | for tag := range tags { 301 | tagList = append(tagList, tag.value) 302 | } 303 | 304 | return tagList 305 | } 306 | 307 | func (x *Error) mergedTags() tags { 308 | merged := make(tags) 309 | 310 | if cause := x.Unwrap(); cause != nil { 311 | if err := Unwrap(cause); err != nil { 312 | merged = err.mergedTags() 313 | } 314 | } 315 | 316 | for tag := range x.tags { 317 | merged[tag] = struct{}{} 318 | } 319 | 320 | return merged 321 | } 322 | 323 | // LogValue returns slog.Value for structured logging. It's implementation of slog.LogValuer. 324 | // https://pkg.go.dev/log/slog#LogValuer 325 | func (x *Error) LogValue() slog.Value { 326 | if x == nil { 327 | return slog.AnyValue(nil) 328 | } 329 | 330 | attrs := []slog.Attr{ 331 | slog.String("message", x.msg), 332 | } 333 | 334 | var values []any 335 | for k, v := range x.values { 336 | values = append(values, slog.Any(k, v)) 337 | } 338 | attrs = append(attrs, slog.Group("values", values...)) 339 | 340 | var tags []string 341 | for tag := range x.tags { 342 | tags = append(tags, tag.value) 343 | } 344 | attrs = append(attrs, slog.Any("tags", tags)) 345 | 346 | var stacktrace any 347 | var traces []string 348 | for _, st := range x.StackTrace() { 349 | traces = append(traces, fmt.Sprintf("%s:%d %s", st.getFilePath(), st.getLineNumber(), st.getFunctionName())) 350 | } 351 | stacktrace = traces 352 | 353 | attrs = append(attrs, slog.Any("stacktrace", stacktrace)) 354 | 355 | if x.cause != nil { 356 | var errAttr slog.Attr 357 | if lv, ok := x.cause.(slog.LogValuer); ok { 358 | errAttr = slog.Any("cause", lv.LogValue()) 359 | } else { 360 | errAttr = slog.Any("cause", x.cause) 361 | } 362 | attrs = append(attrs, errAttr) 363 | } 364 | 365 | return slog.GroupValue(attrs...) 366 | } 367 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package goerr_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log/slog" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/m-mizutani/goerr/v2" 13 | ) 14 | 15 | func oops() *goerr.Error { 16 | return goerr.New("omg") 17 | } 18 | 19 | func normalError() error { 20 | return fmt.Errorf("red") 21 | } 22 | 23 | func wrapError() *goerr.Error { 24 | err := normalError() 25 | return goerr.Wrap(err, "orange") 26 | } 27 | 28 | func TestNew(t *testing.T) { 29 | err := oops() 30 | v := fmt.Sprintf("%+v", err) 31 | if !strings.Contains(v, "goerr/v2_test.oops") { 32 | t.Error("Stack trace 'goerr/v2_test.oops' is not found") 33 | } 34 | if !strings.Contains(err.Error(), "omg") { 35 | t.Error("Error message is not correct") 36 | } 37 | } 38 | 39 | func TestOptions(t *testing.T) { 40 | var testCases = map[string]struct { 41 | options []goerr.Option 42 | values map[string]interface{} 43 | tags []string 44 | }{ 45 | "empty": { 46 | options: []goerr.Option{}, 47 | values: map[string]interface{}{}, 48 | tags: []string{}, 49 | }, 50 | "single value": { 51 | options: []goerr.Option{goerr.Value("key", "value")}, 52 | values: map[string]interface{}{"key": "value"}, 53 | tags: []string{}, 54 | }, 55 | "multiple values": { 56 | options: []goerr.Option{goerr.Value("key1", "value1"), goerr.Value("key2", "value2")}, 57 | values: map[string]interface{}{"key1": "value1", "key2": "value2"}, 58 | tags: []string{}, 59 | }, 60 | "single tag": { 61 | options: []goerr.Option{goerr.Tag(goerr.NewTag("tag1"))}, 62 | values: map[string]interface{}{}, 63 | tags: []string{"tag1"}, 64 | }, 65 | "multiple tags": { 66 | options: []goerr.Option{goerr.Tag(goerr.NewTag("tag1")), goerr.Tag(goerr.NewTag("tag2"))}, 67 | values: map[string]interface{}{}, 68 | tags: []string{"tag1", "tag2"}, 69 | }, 70 | "values and tags": { 71 | options: []goerr.Option{goerr.Value("key", "value"), goerr.Tag(goerr.NewTag("tag1"))}, 72 | values: map[string]interface{}{"key": "value"}, 73 | tags: []string{"tag1"}, 74 | }, 75 | } 76 | 77 | for name, tc := range testCases { 78 | t.Run(name, func(t *testing.T) { 79 | err := goerr.New("test", tc.options...) 80 | values := err.Values() 81 | if len(values) != len(tc.values) { 82 | t.Errorf("Expected values length to be %d, got %d", len(tc.values), len(values)) 83 | } 84 | for k, v := range tc.values { 85 | if values[k] != v { 86 | t.Errorf("Expected value for key '%s' to be '%v', got '%v'", k, v, values[k]) 87 | } 88 | } 89 | 90 | tags := goerr.Tags(err) 91 | if len(tags) != len(tc.tags) { 92 | t.Errorf("Expected tags length to be %d, got %d", len(tc.tags), len(tags)) 93 | } 94 | for _, tag := range tc.tags { 95 | if !sliceHas(tags, tag) { 96 | t.Errorf("Expected tags to contain '%s'", tag) 97 | } 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func TestWrapError(t *testing.T) { 104 | err := wrapError() 105 | st := fmt.Sprintf("%+v", err) 106 | if !strings.Contains(st, "github.com/m-mizutani/goerr/v2_test.wrapError") { 107 | t.Error("Stack trace 'wrapError' is not found") 108 | } 109 | if !strings.Contains(st, "github.com/m-mizutani/goerr/v2_test.TestWrapError") { 110 | t.Error("Stack trace 'TestWrapError' is not found") 111 | } 112 | if strings.Contains(st, "github.com/m-mizutani/goerr/v2_test.normalError") { 113 | t.Error("Stack trace 'normalError' is found") 114 | } 115 | if !strings.Contains(err.Error(), "orange: red") { 116 | t.Error("Error message is not correct") 117 | } 118 | } 119 | 120 | func TestStackTrace(t *testing.T) { 121 | err := oops() 122 | st := err.Stacks() 123 | if len(st) != 4 { 124 | t.Errorf("Expected stack length of 4, got %d", len(st)) 125 | } 126 | if st[0].Func != "github.com/m-mizutani/goerr/v2_test.oops" { 127 | t.Error("Stack trace 'github.com/m-mizutani/goerr/v2_test.oops' is not found") 128 | } 129 | if !regexp.MustCompile(`/goerr/errors_test\.go$`).MatchString(st[0].File) { 130 | t.Error("Stack trace file is not correct") 131 | } 132 | if st[0].Line != 16 { 133 | t.Errorf("Expected line number 13, got %d", st[0].Line) 134 | } 135 | } 136 | 137 | func TestMultiWrap(t *testing.T) { 138 | err1 := oops() 139 | err2 := goerr.Wrap(err1, "some message") 140 | if err1 == err2 { 141 | t.Error("Expected err1 and err2 to be different") 142 | } 143 | 144 | err3 := goerr.Wrap(err1, "some message") 145 | if err1 == err3 { 146 | t.Error("Expected err1 and err3 to be different") 147 | } 148 | } 149 | 150 | func TestErrorCode(t *testing.T) { 151 | rootErr := goerr.New("something bad") 152 | baseErr1 := goerr.New("oops").ID("code1") 153 | baseErr2 := goerr.New("oops").ID("code2") 154 | 155 | newErr := baseErr1.Wrap(rootErr, goerr.V("v", 1)) 156 | 157 | if !errors.Is(newErr, baseErr1) { 158 | t.Error("Expected newErr to be based on baseErr1") 159 | } 160 | if newErr == baseErr1 { 161 | t.Error("Expected newErr and baseErr1 to be different") 162 | } 163 | if newErr.Values()["v"] == nil { 164 | t.Error("Expected newErr to have a non-nil value for 'v'") 165 | } 166 | if baseErr1.Values()["v"] != nil { 167 | t.Error("Expected baseErr1 to have a nil value for 'v'") 168 | } 169 | if errors.Is(newErr, baseErr2) { 170 | t.Error("Expected newErr to not be based on baseErr2") 171 | } 172 | } 173 | 174 | func TestPrintableWithGoErr(t *testing.T) { 175 | cause := errors.New("test") 176 | err := goerr.Wrap(cause, "oops", goerr.V("blue", "five")).ID("E001") 177 | 178 | p := err.Printable() 179 | if p.Message != "oops" { 180 | t.Errorf("Expected message to be 'oops', got '%s'", p.Message) 181 | } 182 | if p.ID != "E001" { 183 | t.Errorf("Expected ID to be 'E001', got '%s'", p.ID) 184 | } 185 | if s, ok := p.Cause.(string); !ok { 186 | t.Errorf("Expected cause is string, got '%t'", p.Cause) 187 | } else if s != "test" { 188 | t.Errorf("Expected message is 'test', got '%s'", s) 189 | } 190 | if p.Values["blue"] != "five" { 191 | t.Errorf("Expected value for 'blue' to be 'five', got '%v'", p.Values["blue"]) 192 | } 193 | } 194 | 195 | func TestPrintableWithError(t *testing.T) { 196 | cause := goerr.New("test") 197 | err := goerr.Wrap(cause, "oops", goerr.V("blue", "five")).ID("E001") 198 | 199 | p := err.Printable() 200 | if p.Message != "oops" { 201 | t.Errorf("Expected message to be 'oops', got '%s'", p.Message) 202 | } 203 | if p.ID != "E001" { 204 | t.Errorf("Expected ID to be 'E001', got '%s'", p.ID) 205 | } 206 | if cp, ok := p.Cause.(*goerr.Printable); !ok { 207 | t.Errorf("Expected cause is goerr.Printable, got '%t'", p.Cause) 208 | } else if cp.Message != "test" { 209 | t.Errorf("Expected message is 'test', got '%s'", cp.Message) 210 | } 211 | if p.Values["blue"] != "five" { 212 | t.Errorf("Expected value for 'blue' to be 'five', got '%v'", p.Values["blue"]) 213 | } 214 | } 215 | 216 | func TestUnwrap(t *testing.T) { 217 | err1 := goerr.New("omg", goerr.V("color", "five")) 218 | err2 := fmt.Errorf("oops: %w", err1) 219 | 220 | err := goerr.Unwrap(err2) 221 | if err == nil { 222 | t.Error("Expected unwrapped error to be non-nil") 223 | } 224 | values := err.Values() 225 | if values["color"] != "five" { 226 | t.Errorf("Expected value for 'color' to be 'five', got '%v'", values["color"]) 227 | } 228 | } 229 | 230 | func TestErrorString(t *testing.T) { 231 | err := goerr.Wrap(goerr.Wrap(goerr.New("blue"), "orange"), "red") 232 | if err.Error() != "red: orange: blue" { 233 | t.Errorf("Expected error message to be 'red: orange: blue', got '%s'", err.Error()) 234 | } 235 | } 236 | 237 | func TestLoggingNestedError(t *testing.T) { 238 | err1 := goerr.New("e1", goerr.V("color", "orange")) 239 | err2 := goerr.Wrap(err1, "e2", goerr.V("number", "five")) 240 | out := &bytes.Buffer{} 241 | logger := slog.New(slog.NewJSONHandler(out, nil)) 242 | logger.Error("fail", slog.Any("error", err2)) 243 | if !strings.Contains(out.String(), `"number":"five"`) { 244 | t.Errorf("Expected log output to contain '\"number\":\"five\"', got '%s'", out.String()) 245 | } 246 | if !strings.Contains(out.String(), `"color":"orange"`) { 247 | t.Errorf("Expected log output to contain '\"color\":\"orange\"', got '%s'", out.String()) 248 | } 249 | } 250 | 251 | func TestLoggerWithNil(t *testing.T) { 252 | out := &bytes.Buffer{} 253 | var err *goerr.Error 254 | logger := slog.New(slog.NewJSONHandler(out, nil)) 255 | logger.Error("fail", slog.Any("error", err)) 256 | if !strings.Contains(out.String(), `"error":null`) { 257 | t.Errorf("Expected log output to contain '\"error\":null', got '%s'", out.String()) 258 | } 259 | } 260 | 261 | func TestUnstack(t *testing.T) { 262 | t.Run("original stack", func(t *testing.T) { 263 | err := oops() 264 | st := err.Stacks() 265 | if st == nil { 266 | t.Error("Expected stack trace to be nil") 267 | } 268 | if len(st) == 0 { 269 | t.Error("Expected stack trace length to be 0") 270 | } 271 | if st[0].Func != "github.com/m-mizutani/goerr/v2_test.oops" { 272 | t.Errorf("Not expected stack trace func name (github.com/m-mizutani/goerr/v2_test.oops): %s", st[0].Func) 273 | } 274 | }) 275 | 276 | t.Run("unstacked", func(t *testing.T) { 277 | err := oops().Unstack() 278 | st1 := err.Stacks() 279 | if st1 == nil { 280 | t.Error("Expected stack trace to be non-nil") 281 | } 282 | if len(st1) == 0 { 283 | t.Error("Expected stack trace length to be non-zero") 284 | } 285 | if st1[0].Func != "github.com/m-mizutani/goerr/v2_test.TestUnstack.func2" { 286 | t.Errorf("Not expected stack trace func name (github.com/m-mizutani/goerr/v2_test.TestUnstack.func2): %s", st1[0].Func) 287 | } 288 | }) 289 | 290 | t.Run("unstackN with 2", func(t *testing.T) { 291 | err := oops().UnstackN(2) 292 | st2 := err.Stacks() 293 | if st2 == nil { 294 | t.Error("Expected stack trace to be non-nil") 295 | } 296 | if len(st2) == 0 { 297 | t.Error("Expected stack trace length to be non-zero") 298 | } 299 | if st2[0].Func != "testing.tRunner" { 300 | t.Errorf("Not expected stack trace func name (testing.tRunner): %s", st2[0].Func) 301 | } 302 | }) 303 | } 304 | 305 | func sliceHas(s []string, target string) bool { 306 | for _, v := range s { 307 | if v == target { 308 | return true 309 | } 310 | } 311 | return false 312 | } 313 | 314 | func TestTags(t *testing.T) { 315 | t1 := goerr.NewTag("tag1") 316 | t2 := goerr.NewTag("tag2") 317 | 318 | err1 := goerr.New("omg").WithTags(t1) 319 | err2 := fmt.Errorf("oops: %w", err1) 320 | err3 := goerr.Wrap(err2, "orange").WithTags(t2) 321 | err4 := fmt.Errorf("oh no: %w", err3) 322 | 323 | tags := goerr.Tags(err4) 324 | if len(tags) != 2 { 325 | t.Errorf("Expected tags length to be 2, got %d", len(tags)) 326 | } 327 | if !sliceHas(tags, "tag1") { 328 | t.Error("Expected tags to contain 'tag1'") 329 | } 330 | if !sliceHas(tags, "tag2") { 331 | t.Error("Expected tags to contain 'tag2'") 332 | } 333 | } 334 | 335 | func TestValues(t *testing.T) { 336 | err1 := goerr.New("omg", goerr.V("color", "blue")) 337 | err2 := fmt.Errorf("oops: %w", err1) 338 | err3 := goerr.Wrap(err2, "red", goerr.V("number", "five")) 339 | err4 := fmt.Errorf("oh no: %w", err3) 340 | 341 | values := goerr.Values(err4) 342 | if len(values) != 2 { 343 | t.Errorf("Expected values length to be 2, got %d", len(values)) 344 | } 345 | if values["color"] != "blue" { 346 | t.Errorf("Expected value for 'color' to be 'blue', got '%v'", values["color"]) 347 | } 348 | if values["number"] != "five" { 349 | t.Errorf("Expected value for 'number' to be 'five', got '%v'", values["number"]) 350 | } 351 | } 352 | 353 | func TestFormat(t *testing.T) { 354 | err := goerr.New("omg", goerr.V("color", "blue"), goerr.V("number", 123)) 355 | 356 | b := &bytes.Buffer{} 357 | fmt.Fprintf(b, "%+v", err) 358 | if !strings.Contains(b.String(), "color: blue") { 359 | t.Errorf("Expected log output to contain 'color: blue', got '%s'", b.String()) 360 | } 361 | if !strings.Contains(b.String(), "number: 123") { 362 | t.Errorf("Expected log output to contain 'number: 123', got '%s'", b.String()) 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "time" 7 | 8 | "github.com/m-mizutani/goerr/v2" 9 | ) 10 | 11 | func someAction(input string) error { 12 | if input != "OK" { 13 | return goerr.New("input is not OK", 14 | goerr.Value("input", input), 15 | goerr.Value("time", time.Now()), 16 | ) 17 | } 18 | return nil 19 | } 20 | 21 | func main() { 22 | if err := someAction("ng"); err != nil { 23 | var goErr *goerr.Error 24 | if errors.As(err, &goErr) { 25 | for k, v := range goErr.Values() { 26 | log.Printf("%s = %v\n", k, v) 27 | } 28 | } 29 | log.Fatalf("Error: %+v\n", err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/builder/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log/slog" 5 | 6 | "github.com/m-mizutani/goerr/v2" 7 | ) 8 | 9 | type object struct { 10 | id string 11 | color string 12 | } 13 | 14 | func (o *object) Validate() error { 15 | eb := goerr.NewBuilder().With(goerr.Value("id", o.id)) 16 | 17 | if o.color == "" { 18 | return eb.New("color is empty") 19 | } 20 | 21 | return nil 22 | } 23 | 24 | func main() { 25 | obj := &object{id: "object-1"} 26 | 27 | if err := obj.Validate(); err != nil { 28 | slog.Default().Error("Validation error", "err", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/errors_is/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/m-mizutani/goerr/v2" 8 | ) 9 | 10 | var errInvalidInput = errors.New("invalid input") 11 | 12 | func someAction(input string) error { 13 | if input != "OK" { 14 | return goerr.Wrap(errInvalidInput, "input is not OK", goerr.Value("input", input)) 15 | } 16 | // ..... 17 | return nil 18 | } 19 | 20 | func main() { 21 | if err := someAction("ng"); err != nil { 22 | switch { 23 | case errors.Is(err, errInvalidInput): 24 | log.Printf("It's user's bad: %v\n", err) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/logging/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | 7 | "log/slog" 8 | 9 | "github.com/m-mizutani/goerr/v2" 10 | ) 11 | 12 | var errRuntime = errors.New("runtime error") 13 | 14 | func someAction(input string) error { 15 | if err := validate(input); err != nil { 16 | return goerr.Wrap(err, "failed validation") 17 | } 18 | return nil 19 | } 20 | 21 | func validate(input string) error { 22 | if input != "OK" { 23 | return goerr.Wrap(errRuntime, "invalid input", goerr.V("input", input)) 24 | } 25 | return nil 26 | } 27 | 28 | func main() { 29 | logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 30 | if err := someAction("ng"); err != nil { 31 | logger.Error("aborted myapp", slog.Any("error", err)) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/stacktrace_extract/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/m-mizutani/goerr/v2" 8 | ) 9 | 10 | func someAction(fname string) error { 11 | if _, err := os.Open(fname); err != nil { 12 | return goerr.Wrap(err, "failed to open file") 13 | } 14 | return nil 15 | } 16 | 17 | func main() { 18 | if err := someAction("no_such_file.txt"); err != nil { 19 | /* 20 | // NOTE: errors.As also works 21 | var goErr *goerr.Error 22 | if errors.As(err, &goErr); goErr != nil { 23 | */ 24 | if goErr := goerr.Unwrap(err); goErr != nil { 25 | for i, st := range goErr.Stacks() { 26 | log.Printf("%d: %+v\n", i, st) 27 | } 28 | } 29 | log.Fatal(err) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/stacktrace_print/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | 7 | "github.com/m-mizutani/goerr/v2" 8 | ) 9 | 10 | func nestedAction2() error { 11 | return errors.New("fatal error in the nested action2") 12 | } 13 | 14 | func nestedAction() error { 15 | return goerr.Wrap(nestedAction2(), "nestedAction2 failed") 16 | } 17 | 18 | func someAction() error { 19 | return goerr.Wrap(nestedAction(), "nestedAction failed") 20 | } 21 | 22 | func main() { 23 | if err := someAction(); err != nil { 24 | log.Fatalf("%+v", err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/tag/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/m-mizutani/goerr/v2" 7 | ) 8 | 9 | var ( 10 | ErrTagSysError = goerr.NewTag("system_error") 11 | ErrTagBadRequest = goerr.NewTag("bad_request") 12 | ) 13 | 14 | func handleError(w http.ResponseWriter, err error) { 15 | if goErr := goerr.Unwrap(err); goErr != nil { 16 | switch { 17 | case goErr.HasTag(ErrTagSysError): 18 | w.WriteHeader(http.StatusInternalServerError) 19 | case goErr.HasTag(ErrTagBadRequest): 20 | w.WriteHeader(http.StatusBadRequest) 21 | default: 22 | w.WriteHeader(http.StatusInternalServerError) 23 | } 24 | } else { 25 | w.WriteHeader(http.StatusInternalServerError) 26 | } 27 | _, _ = w.Write([]byte(err.Error())) 28 | } 29 | 30 | func someAction() error { 31 | if _, err := http.Get("http://example.com/some/resource"); err != nil { 32 | return goerr.Wrap(err, "failed to get some resource", goerr.T(ErrTagSysError)) 33 | } 34 | return nil 35 | } 36 | 37 | func main() { 38 | http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { 39 | if err := someAction(); err != nil { 40 | handleError(w, err) 41 | return 42 | } 43 | w.WriteHeader(http.StatusOK) 44 | _, _ = w.Write([]byte("OK")) 45 | }) 46 | 47 | // #nosec 48 | http.ListenAndServe(":8090", nil) 49 | } 50 | -------------------------------------------------------------------------------- /examples/variables/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "strings" 7 | 8 | "github.com/m-mizutani/goerr/v2" 9 | ) 10 | 11 | var errFormatMismatch = errors.New("format mismatch") 12 | 13 | func someAction(tasks []task) error { 14 | for _, t := range tasks { 15 | if err := validateData(t.Data); err != nil { 16 | return goerr.Wrap(err, "failed to validate data", goerr.Value("name", t.Name)) 17 | } 18 | } 19 | // .... 20 | return nil 21 | } 22 | 23 | func validateData(data string) error { 24 | if !strings.HasPrefix(data, "data:") { 25 | return goerr.Wrap(errFormatMismatch, "validation error", goerr.V("data", data)) 26 | } 27 | return nil 28 | } 29 | 30 | type task struct { 31 | Name string 32 | Data string 33 | } 34 | 35 | func main() { 36 | tasks := []task{ 37 | {Name: "task1", Data: "data:1"}, 38 | {Name: "task2", Data: "invalid"}, 39 | {Name: "task3", Data: "data:3"}, 40 | } 41 | if err := someAction(tasks); err != nil { 42 | if goErr := goerr.Unwrap(err); goErr != nil { 43 | for k, v := range goErr.Values() { 44 | log.Printf("var: %s => %v\n", k, v) 45 | } 46 | } 47 | log.Fatalf("msg: %s", err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/m-mizutani/goerr/v2 2 | 3 | go 1.21 4 | 5 | require github.com/google/uuid v1.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 2 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 3 | -------------------------------------------------------------------------------- /stack.go: -------------------------------------------------------------------------------- 1 | package goerr 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "path" 7 | "runtime" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | // Stack represents function, file and line No of stack trace 13 | type Stack struct { 14 | Func string `json:"func"` 15 | File string `json:"file"` 16 | Line int `json:"line"` 17 | } 18 | 19 | // Stacks returns stack trace array generated by pkg/errors 20 | func (x *Error) Stacks() []*Stack { 21 | if x.st == nil { 22 | return nil 23 | } 24 | 25 | stacks := make([]*Stack, 0, len(*x.st)) 26 | for _, pc := range *x.st { 27 | f := newFrame(pc) 28 | stacks = append(stacks, &Stack{ 29 | Func: f.getFunctionName(), 30 | File: f.getFilePath(), 31 | Line: f.getLineNumber(), 32 | }) 33 | } 34 | return stacks 35 | } 36 | 37 | // StackTrace returns stack trace that is compatible with pkg/errors 38 | func (x *Error) StackTrace() StackTrace { 39 | if x.st == nil { 40 | return nil 41 | } 42 | return x.st.toStackTrace() 43 | } 44 | 45 | // frame represents a single stack frame 46 | type frame uintptr 47 | 48 | // stack represents a stack of program counters 49 | type stack []uintptr 50 | 51 | // StackTrace is array of frame. It's exported for compatibility with github.com/pkg/errors 52 | type StackTrace []frame 53 | 54 | // newFrame creates a new frame from program counter 55 | func newFrame(pc uintptr) frame { 56 | return frame(pc - 1) 57 | } 58 | 59 | // pc returns the program counter for this frame 60 | func (f frame) pc() uintptr { 61 | return uintptr(f) 62 | } 63 | 64 | // getFilePath returns the full path to the file that contains the function 65 | func (f frame) getFilePath() string { 66 | fn := runtime.FuncForPC(f.pc()) 67 | if fn == nil { 68 | return "unknown" 69 | } 70 | file, _ := fn.FileLine(f.pc()) 71 | return file 72 | } 73 | 74 | // getLineNumber returns the line number of source code 75 | func (f frame) getLineNumber() int { 76 | fn := runtime.FuncForPC(f.pc()) 77 | if fn == nil { 78 | return 0 79 | } 80 | _, line := fn.FileLine(f.pc()) 81 | return line 82 | } 83 | 84 | // getFunctionName returns the name of this function 85 | func (f frame) getFunctionName() string { 86 | fn := runtime.FuncForPC(f.pc()) 87 | if fn == nil { 88 | return "unknown" 89 | } 90 | return fn.Name() 91 | } 92 | 93 | // Format implements fmt.Formatter interface 94 | func (f frame) Format(s fmt.State, verb rune) { 95 | switch verb { 96 | case 's': 97 | if s.Flag('+') { 98 | f.formatWithFunctionName(s) 99 | } else { 100 | f.formatBaseFileName(s) 101 | } 102 | case 'd': 103 | f.formatLineNumber(s) 104 | case 'n': 105 | f.formatShortFunctionName(s) 106 | case 'v': 107 | f.formatVerbose(s) 108 | } 109 | } 110 | 111 | func (f frame) formatWithFunctionName(s fmt.State) { 112 | _, _ = io.WriteString(s, f.getFunctionName()) 113 | _, _ = io.WriteString(s, "\n\t") 114 | _, _ = io.WriteString(s, f.getFilePath()) 115 | } 116 | 117 | func (f frame) formatBaseFileName(s fmt.State) { 118 | _, _ = io.WriteString(s, path.Base(f.getFilePath())) 119 | } 120 | 121 | func (f frame) formatLineNumber(s fmt.State) { 122 | _, _ = io.WriteString(s, strconv.Itoa(f.getLineNumber())) 123 | } 124 | 125 | func (f frame) formatShortFunctionName(s fmt.State) { 126 | _, _ = io.WriteString(s, getShortFunctionName(f.getFunctionName())) 127 | } 128 | 129 | func (f frame) formatVerbose(s fmt.State) { 130 | f.Format(s, 's') 131 | _, _ = io.WriteString(s, ":") 132 | f.Format(s, 'd') 133 | } 134 | 135 | // MarshalText implements encoding.TextMarshaler interface 136 | func (f frame) MarshalText() ([]byte, error) { 137 | name := f.getFunctionName() 138 | if name == "unknown" { 139 | return []byte(name), nil 140 | } 141 | return []byte(fmt.Sprintf("%s %s:%d", name, f.getFilePath(), f.getLineNumber())), nil 142 | } 143 | 144 | // Format implements fmt.Formatter interface for StackTrace 145 | func (st StackTrace) Format(s fmt.State, verb rune) { 146 | switch verb { 147 | case 'v': 148 | if s.Flag('+') { 149 | st.formatVerbose(s) 150 | } else if s.Flag('#') { 151 | st.formatGoSyntax(s) 152 | } else { 153 | st.formatSlice(s, verb) 154 | } 155 | case 's': 156 | st.formatSlice(s, verb) 157 | } 158 | } 159 | 160 | func (st StackTrace) formatVerbose(s fmt.State) { 161 | for _, f := range st { 162 | _, _ = io.WriteString(s, "\n") 163 | f.Format(s, 'v') 164 | } 165 | } 166 | 167 | func (st StackTrace) formatGoSyntax(s fmt.State) { 168 | fmt.Fprintf(s, "%#v", []frame(st)) 169 | } 170 | 171 | func (st StackTrace) formatSlice(s fmt.State, verb rune) { 172 | _, _ = io.WriteString(s, "[") 173 | for i, f := range st { 174 | if i > 0 { 175 | _, _ = io.WriteString(s, " ") 176 | } 177 | f.Format(s, verb) 178 | } 179 | _, _ = io.WriteString(s, "]") 180 | } 181 | 182 | // Format implements fmt.Formatter interface for stack 183 | func (s *stack) Format(st fmt.State, verb rune) { 184 | if verb == 'v' && st.Flag('+') { 185 | for _, pc := range *s { 186 | f := newFrame(pc) 187 | fmt.Fprintf(st, "\n%+v", f) 188 | } 189 | } 190 | } 191 | 192 | // toStackTrace converts stack to StackTrace 193 | func (s *stack) toStackTrace() StackTrace { 194 | if s == nil { 195 | return nil 196 | } 197 | frames := make([]frame, len(*s)) 198 | for i, pc := range *s { 199 | frames[i] = newFrame(pc) 200 | } 201 | return frames 202 | } 203 | 204 | // unstack trims the stack trace by n frames 205 | func unstack(st *stack, n int) *stack { 206 | if st == nil { 207 | return &stack{} 208 | } 209 | 210 | switch { 211 | case n <= 0: 212 | return &stack{} 213 | case n >= len(*st): 214 | return st 215 | default: 216 | trimmed := (*st)[n:] 217 | return &trimmed 218 | } 219 | } 220 | 221 | // callers returns the stack of program counters 222 | func callers() *stack { 223 | const depth = 32 224 | var pcs [depth]uintptr 225 | n := runtime.Callers(4, pcs[:]) 226 | st := stack(pcs[:n]) 227 | return &st 228 | } 229 | 230 | // getShortFunctionName removes the path prefix component of a function's name 231 | func getShortFunctionName(name string) string { 232 | parts := strings.Split(name, "/") 233 | lastPart := parts[len(parts)-1] 234 | dotIndex := strings.Index(lastPart, ".") 235 | if dotIndex == -1 { 236 | return lastPart 237 | } 238 | return lastPart[dotIndex+1:] 239 | } 240 | -------------------------------------------------------------------------------- /tag.go: -------------------------------------------------------------------------------- 1 | package goerr 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // Tag is a type to represent an error tag. It is used to categorize errors. The struct should be created by only NewTag function. 9 | // 10 | // Example: 11 | // 12 | // TagNotFound := NewTag("not_found") 13 | // 14 | // func FindUser(id string) (*User, error) { 15 | // ... 16 | // if user == nil { 17 | // return nil, goerr.New("user not found", goerr.Tag(TagNotFound)) 18 | // } 19 | // ... 20 | // } 21 | // 22 | // func main() { 23 | // err := FindUser("123") 24 | // if goErr := goerr.Unwrap(err); goErr != nil { 25 | // if goErr.HasTag(TagNotFound) { 26 | // fmt.Println("User not found") 27 | // } 28 | // } 29 | // } 30 | type tag struct { 31 | value string 32 | } 33 | 34 | // NewTag creates a new Tag. The key will be empty. 35 | func NewTag(value string) tag { 36 | return tag{value: value} 37 | } 38 | 39 | // String returns the string representation of the Tag. It's for implementing fmt.Stringer interface. 40 | func (t tag) String() string { 41 | return t.value 42 | } 43 | 44 | // Format writes the Tag to the writer. It's for implementing fmt.Formatter interface. 45 | func (t tag) Format(s fmt.State, verb rune) { 46 | _, _ = io.WriteString(s, t.value) 47 | } 48 | 49 | // WithTags adds tags to the error. The tags are used to categorize errors. 50 | // 51 | // Deprecated: Use goerr.Tag instead. 52 | func (x *Error) WithTags(tags ...tag) *Error { 53 | for _, tag := range tags { 54 | x.tags[tag] = struct{}{} 55 | } 56 | return x 57 | } 58 | 59 | // HasTag returns true if the error has the tag. 60 | func (x *Error) HasTag(tag tag) bool { 61 | tags := x.mergedTags() 62 | _, ok := tags[tag] 63 | return ok 64 | } 65 | 66 | type tags map[tag]struct{} 67 | 68 | func (t tags) clone() tags { 69 | newTags := make(tags) 70 | for tag := range t { 71 | newTags[tag] = struct{}{} 72 | } 73 | return newTags 74 | } 75 | -------------------------------------------------------------------------------- /tag_test.go: -------------------------------------------------------------------------------- 1 | package goerr_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/m-mizutani/goerr/v2" 8 | ) 9 | 10 | func ExampleNewTag() { 11 | t1 := goerr.NewTag("DB error") 12 | err := goerr.New("error message", goerr.Tag(t1)) 13 | 14 | if goErr := goerr.Unwrap(err); goErr != nil { 15 | if goErr.HasTag(t1) { 16 | fmt.Println("DB error") 17 | } 18 | } 19 | // Output: DB error 20 | } 21 | 22 | func TestNewTag(t *testing.T) { 23 | tagValue := "test_tag" 24 | tag := goerr.NewTag(tagValue) 25 | 26 | if tag.String() != tagValue { 27 | t.Errorf("expected tag value to be %s, got %s", tagValue, tag.String()) 28 | } 29 | } 30 | 31 | func TestWithTags(t *testing.T) { 32 | tag1 := goerr.NewTag("tag1") 33 | tag2 := goerr.NewTag("tag2") 34 | tag3 := goerr.NewTag("tag3") 35 | err := goerr.New("error message", goerr.Tag(tag1), goerr.Tag(tag2)) 36 | 37 | if goErr := goerr.Unwrap(err); goErr != nil { 38 | if !goErr.HasTag(tag1) { 39 | t.Errorf("expected error to have tag1") 40 | } 41 | if !goErr.HasTag(tag2) { 42 | t.Errorf("expected error to have tag2") 43 | } 44 | if goErr.HasTag(tag3) { 45 | t.Errorf("expected error to not have tag3") 46 | } 47 | } 48 | } 49 | 50 | func TestHasTag(t *testing.T) { 51 | tag := goerr.NewTag("test_tag") 52 | err := goerr.New("error message", goerr.Tag(tag)) 53 | 54 | if goErr := goerr.Unwrap(err); goErr != nil { 55 | if !goErr.HasTag(tag) { 56 | t.Errorf("expected error to have tag") 57 | } 58 | } 59 | } 60 | --------------------------------------------------------------------------------