Skip to content

Commit 144a742

Browse files
authored
Merge pull request #2 from zignd/fix-errors-serialization
Fix errors serialization
2 parents 07b2b57 + d00bbcb commit 144a742

File tree

7 files changed

+204
-75
lines changed

7 files changed

+204
-75
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ _testmain.go
2222
*.exe
2323
*.test
2424
*.prof
25+
26+
cover.*

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
tests:
2-
go test -v ./...
2+
go test -v -coverprofile=cover.out ./...
3+
go tool cover -html=cover.out -o=cover.html

README.md

Lines changed: 51 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -97,69 +97,71 @@ func main() {
9797
Here's the execution of the example:
9898

9999
```
100-
$ go run examples/example1/example1.go
100+
$ go run examples/example1/example1.go
101101
Error logged as a JSON structure using the JSON.MarshalIndent:
102-
{
103-
"message": "failed to complete the transaction on bank_123456",
104-
"data": {
105-
"transactionId": "tx_123456",
106-
"userId": "67890"
102+
[
103+
{
104+
"data": {
105+
"transactionId": "tx_123456",
106+
"userId": "67890"
107+
},
108+
"message": "failed to complete the transaction on bank_123456",
109+
"stack": [
110+
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13",
111+
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
112+
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
113+
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
114+
]
107115
},
108-
"stack": [
109-
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13",
110-
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
111-
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
112-
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
113-
],
114-
"cause": {
115-
"message": "failed to update the database",
116+
{
116117
"data": {
117118
"operation": "update",
118119
"tableName": "transactions"
119120
},
121+
"message": "failed to update the database",
120122
"stack": [
121123
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24",
122124
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
123125
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
124126
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
125127
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
126-
],
127-
"cause": {
128-
"message": "connection timeout",
129-
"data": {
130-
"server": "db-server-01",
131-
"timeoutSeconds": 30
132-
},
133-
"stack": [
134-
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:35",
135-
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
136-
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
137-
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
138-
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
139-
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
140-
],
141-
"cause": {
142-
"message": "network instability detected",
143-
"data": {
144-
"network": "internal",
145-
"severity": "high"
146-
},
147-
"stack": [
148-
"main.open @ /root/hack/errors/examples/example1/example1.go:45",
149-
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:34",
150-
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
151-
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
152-
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
153-
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
154-
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
155-
]
156-
}
157-
}
128+
]
129+
},
130+
{
131+
"data": {
132+
"server": "db-server-01",
133+
"timeoutSeconds": 30
134+
},
135+
"message": "connection timeout",
136+
"stack": [
137+
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:35",
138+
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
139+
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
140+
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
141+
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
142+
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
143+
]
144+
},
145+
{
146+
"data": {
147+
"network": "internal",
148+
"severity": "high"
149+
},
150+
"message": "network instability detected",
151+
"stack": [
152+
"main.open @ /root/hack/errors/examples/example1/example1.go:45",
153+
"main.createConnection @ /root/hack/errors/examples/example1/example1.go:34",
154+
"main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23",
155+
"main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12",
156+
"main.main @ /root/hack/errors/examples/example1/example1.go:52",
157+
"runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194",
158+
"runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"
159+
]
158160
}
159-
}
161+
]
160162
161163
Error logged as a JSON structure using the JSON.Marshal:
162-
{"message":"failed to complete the transaction on bank_123456","data":{"transactionId":"tx_123456","userId":"67890"},"stack":["main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"failed to update the database","data":{"operation":"update","tableName":"transactions"},"stack":["main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"connection timeout","data":{"server":"db-server-01","timeoutSeconds":30},"stack":["main.createConnection @ /root/hack/errors/examples/example1/example1.go:35","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"],"cause":{"message":"network instability detected","data":{"network":"internal","severity":"high"},"stack":["main.open @ /root/hack/errors/examples/example1/example1.go:45","main.createConnection @ /root/hack/errors/examples/example1/example1.go:34","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]}}}}
164+
[{"data":{"transactionId":"tx_123456","userId":"67890"},"message":"failed to complete the transaction on bank_123456","stack":["main.createTransaction @ /root/hack/errors/examples/example1/example1.go:13","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"operation":"update","tableName":"transactions"},"message":"failed to update the database","stack":["main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:24","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"server":"db-server-01","timeoutSeconds":30},"message":"connection timeout","stack":["main.createConnection @ /root/hack/errors/examples/example1/example1.go:35","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]},{"data":{"network":"internal","severity":"high"},"message":"network instability detected","stack":["main.open @ /root/hack/errors/examples/example1/example1.go:45","main.createConnection @ /root/hack/errors/examples/example1/example1.go:34","main.updateDatabase @ /root/hack/errors/examples/example1/example1.go:23","main.createTransaction @ /root/hack/errors/examples/example1/example1.go:12","main.main @ /root/hack/errors/examples/example1/example1.go:52","runtime/internal/atomic.(*Uint32).Load @ /root/go/version/go1.21.0/src/runtime/internal/atomic/types.go:194","runtime.goexit @ /root/go/version/go1.21.0/src/runtime/asm_amd64.s:1651"]}]
163165
164166
Error logged using the s format specifier:
165167
failed to complete the transaction on bank_123456: failed to update the database: connection timeout: network instability detected
@@ -204,8 +206,8 @@ cause:
204206
message:
205207
"network instability detected"
206208
data:
207-
network: internal
208209
severity: high
210+
network: internal
209211
stack:
210212
main.open @ /root/hack/errors/examples/example1/example1.go:45
211213
main.createConnection @ /root/hack/errors/examples/example1/example1.go:34

convertion.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package errors
2+
3+
import "errors"
4+
5+
// toMapsSlice converts an error and its causes to flat slice of maps where each map represents an error.
6+
func toMapsSlice(err error) []map[string]any {
7+
errMaps := make([]map[string]any, 0)
8+
9+
if err == nil {
10+
return errMaps
11+
}
12+
13+
currentErr := err
14+
for {
15+
errMap, errCause := toMapAndCause(currentErr)
16+
errMaps = append(errMaps, errMap)
17+
if errCause == nil {
18+
break
19+
}
20+
currentErr = errCause
21+
}
22+
23+
return errMaps
24+
}
25+
26+
// toMapAndCause converts an error to a map and extracts the cause.
27+
func toMapAndCause(err error) (map[string]any, error) {
28+
errMap := make(map[string]any)
29+
var errCause error
30+
31+
if e, ok := err.(*Err); ok {
32+
errMap["message"] = e.Message
33+
errMap["data"] = e.Data
34+
errMap["stack"] = e.Stack
35+
errCause = e.Cause
36+
} else {
37+
errMap["message"] = err.Error()
38+
errCause = errors.Unwrap(err)
39+
}
40+
41+
return errMap, errCause
42+
}

error.go

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package errors
22

3-
import "fmt"
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
)
47

58
// Err is the error struct used internally by the package. This type should only be used for type assertions.
69
type Err struct {
@@ -31,28 +34,6 @@ func (e Err) Unwrap() error {
3134
return e.Cause
3235
}
3336

34-
// WithStack adds a stack trace to the provided error if it is an Err or *Err.
35-
func WithStack(err error) error {
36-
if e, ok := err.(Err); ok {
37-
e.Stack = callers()
38-
return e
39-
} else if e, ok := err.(*Err); ok {
40-
e.Stack = callers()
41-
return e
42-
} else {
43-
return err
44-
}
45-
}
46-
47-
// WithCause adds a cause to the provided error if it is an Err or *Err.
48-
func WithCause(err error, cause error) error {
49-
if e, ok := err.(Err); ok {
50-
e.Cause = cause
51-
return e
52-
} else if e, ok := err.(*Err); ok {
53-
e.Cause = cause
54-
return e
55-
} else {
56-
return err
57-
}
37+
func (e *Err) MarshalJSON() ([]byte, error) {
38+
return json.Marshal(toMapsSlice(e))
5839
}

error_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package errors
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"testing"
7+
)
8+
9+
func TestJSONMarshaling(t *testing.T) {
10+
t.Run("when marshaling a nested chain of errors.Err errors, should marshal the full chain", func(t *testing.T) {
11+
err1 := New("context timeout")
12+
err2 := Wrap(err1, "failed to connect to the database")
13+
err3 := Wrap(err2, "failed to start the server")
14+
15+
b, err := json.MarshalIndent(err3, "", " ")
16+
if err != nil {
17+
t.Fatalf("unexpected error: %v", err)
18+
}
19+
20+
var errs []map[string]any
21+
err = json.Unmarshal(b, &errs)
22+
if err != nil {
23+
t.Fatalf("unexpected error: %v", err)
24+
}
25+
26+
if len(errs) != 3 {
27+
t.Fatalf("unexpected number of errors, got %d, expected %d", len(errs), 3)
28+
}
29+
30+
if fmt.Sprint(errs[0]["message"]) != err3.(*Err).Message {
31+
t.Errorf("unexpected error message, got %q, expected %q", errs[0]["message"], err3.(*Err).Message)
32+
}
33+
34+
if fmt.Sprint(errs[1]["message"]) != err2.(*Err).Message {
35+
t.Errorf("unexpected error message, got %q, expected %q", errs[1]["message"], err2.(*Err).Message)
36+
}
37+
38+
if fmt.Sprint(errs[2]["message"]) != err1.(*Err).Message {
39+
t.Errorf("unexpected error message, got %q, expected %q", errs[2]["message"], err1.(*Err).Message)
40+
}
41+
})
42+
43+
t.Run("when marshaling a chain of errors.Err and standard errors, should marshal the full chain", func(t *testing.T) {
44+
err1 := New("context timeout")
45+
err2 := fmt.Errorf("failed to connect to the database: %w", err1)
46+
err3 := Wrap(err2, "failed to start the server")
47+
48+
b, err := json.MarshalIndent(err3, "", " ")
49+
if err != nil {
50+
t.Fatalf("unexpected error: %v", err)
51+
}
52+
53+
var errs []map[string]any
54+
err = json.Unmarshal(b, &errs)
55+
if err != nil {
56+
t.Fatalf("unexpected error: %v", err)
57+
}
58+
59+
if len(errs) != 3 {
60+
t.Fatalf("unexpected number of errors, got %d, expected %d", len(errs), 3)
61+
}
62+
63+
if fmt.Sprint(errs[0]["message"]) != err3.(*Err).Message {
64+
t.Errorf("unexpected error message, got %q, expected %q", errs[0]["message"], err3.(*Err).Message)
65+
}
66+
67+
if fmt.Sprint(errs[1]["message"]) != err2.Error() {
68+
t.Errorf("unexpected error message, got %q, expected %q", errs[1]["message"], err2.Error())
69+
}
70+
71+
if fmt.Sprint(errs[2]["message"]) != err1.(*Err).Message {
72+
t.Errorf("unexpected error message, got %q, expected %q", errs[2]["message"], err1.(*Err).Message)
73+
}
74+
})
75+
}

errors.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,29 @@ func Wrapdf(err error, data Data, format string, args ...any) error {
7575
Cause: err,
7676
}
7777
}
78+
79+
// WithStack adds a stack trace to the provided error if it is an Err or *Err.
80+
func WithStack(err error) error {
81+
if e, ok := err.(Err); ok {
82+
e.Stack = callers()
83+
return e
84+
} else if e, ok := err.(*Err); ok {
85+
e.Stack = callers()
86+
return e
87+
} else {
88+
return err
89+
}
90+
}
91+
92+
// WithCause adds a cause to the provided error if it is an Err or *Err.
93+
func WithCause(err error, cause error) error {
94+
if e, ok := err.(Err); ok {
95+
e.Cause = cause
96+
return e
97+
} else if e, ok := err.(*Err); ok {
98+
e.Cause = cause
99+
return e
100+
} else {
101+
return err
102+
}
103+
}

0 commit comments

Comments
 (0)