Skip to content

Commit 71ab9cb

Browse files
committed
feat(errors): enhance error handling by improving JSON marshaling and logging structure, adding tests for nested errors and failing marshaler scenarios
1 parent f0630dc commit 71ab9cb

File tree

4 files changed

+420
-44
lines changed

4 files changed

+420
-44
lines changed

error.go

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"sort"
89

910
"log/slog"
1011
)
@@ -93,15 +94,16 @@ func Tags(err error) []string {
9394

9495
// HasTag returns true if the error has the tag.
9596
func HasTag(err error, tag tag) bool {
96-
if e := Unwrap(err); e != nil {
97-
return e.HasTag(tag)
98-
}
99-
100-
// Check for Errors type using AsErrors
97+
// Check for Errors type first using AsErrors
10198
if errs := AsErrors(err); errs != nil {
10299
return errs.HasTag(tag)
103100
}
104101

102+
// Check for Error type using Unwrap
103+
if e := Unwrap(err); e != nil {
104+
return e.HasTag(tag)
105+
}
106+
105107
return false
106108
}
107109

@@ -219,17 +221,34 @@ func (x *Error) Format(s fmt.State, verb rune) {
219221
c.st.Format(s, verb)
220222
_, _ = io.WriteString(s, "\n")
221223

222-
if len(x.values) > 0 {
224+
// Use merged values from entire error chain
225+
mergedValues := x.Values()
226+
if len(mergedValues) > 0 {
223227
_, _ = io.WriteString(s, "\nValues:\n")
224-
for k, v := range x.values {
225-
_, _ = io.WriteString(s, fmt.Sprintf(" %s: %v\n", k, v))
228+
// Sort keys for predictable output
229+
keys := make([]string, 0, len(mergedValues))
230+
for k := range mergedValues {
231+
keys = append(keys, k)
232+
}
233+
sort.Strings(keys)
234+
for _, k := range keys {
235+
_, _ = io.WriteString(s, fmt.Sprintf(" %s: %v\n", k, mergedValues[k]))
226236
}
227237
_, _ = io.WriteString(s, "\n")
228238
}
229-
if len(x.typedValues) > 0 {
239+
240+
// Use merged typed values from entire error chain
241+
mergedTypedValues := x.TypedValues()
242+
if len(mergedTypedValues) > 0 {
230243
_, _ = io.WriteString(s, "\nTyped Values:\n")
231-
for k, v := range x.typedValues {
232-
_, _ = io.WriteString(s, fmt.Sprintf(" %s: %v\n", k, v))
244+
// Sort keys for predictable output
245+
keys := make([]string, 0, len(mergedTypedValues))
246+
for k := range mergedTypedValues {
247+
keys = append(keys, k)
248+
}
249+
sort.Strings(keys)
250+
for _, k := range keys {
251+
_, _ = io.WriteString(s, fmt.Sprintf(" %s: %v\n", k, mergedTypedValues[k]))
233252
}
234253
_, _ = io.WriteString(s, "\n")
235254
}
@@ -285,26 +304,12 @@ func (x *Error) Wrap(cause error, options ...Option) *Error {
285304

286305
// 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.
287306
func (x *Error) Values() map[string]any {
288-
values := x.mergedValues()
289-
290-
// Add string-based values
291-
for key, value := range x.values {
292-
values[key] = value
293-
}
294-
295-
return values
307+
return x.mergedValues()
296308
}
297309

298310
// TypedValues returns map of key and value that is set by TypedValue. All wrapped goerr.Error typed key and values will be merged. Key and values of wrapped error is overwritten by upper goerr.Error.
299311
func (x *Error) TypedValues() map[string]any {
300-
typedValues := x.mergedTypedValues()
301-
302-
// Add typed values
303-
for key, value := range x.typedValues {
304-
typedValues[key] = value
305-
}
306-
307-
return typedValues
312+
return x.mergedTypedValues()
308313
}
309314

310315
func (x *Error) mergedValues() values {

error_test.go

Lines changed: 109 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestNew(t *testing.T) {
2020
// Test with options
2121
tag := goerr.NewTag("test")
2222
err = goerr.New("test error", goerr.Tag(tag), goerr.Value("key", "value"))
23-
23+
2424
if !err.HasTag(tag) {
2525
t.Error("Error should have the tag")
2626
}
@@ -46,7 +46,7 @@ func TestWrap(t *testing.T) {
4646

4747
func TestUnwrap(t *testing.T) {
4848
err := goerr.New("test error")
49-
49+
5050
// Test unwrapping goerr.Error
5151
unwrapped := goerr.Unwrap(err)
5252
if unwrapped == nil {
@@ -62,7 +62,7 @@ func TestUnwrap(t *testing.T) {
6262
}
6363

6464
func TestErrorValues(t *testing.T) {
65-
err := goerr.New("test error",
65+
err := goerr.New("test error",
6666
goerr.Value("key1", "value1"),
6767
goerr.Value("key2", 42))
6868

@@ -85,7 +85,7 @@ func TestErrorValues(t *testing.T) {
8585
func TestErrorTags(t *testing.T) {
8686
tag1 := goerr.NewTag("tag1")
8787
tag2 := goerr.NewTag("tag2")
88-
88+
8989
err := goerr.New("test error", goerr.Tag(tag1), goerr.Tag(tag2))
9090

9191
tags := goerr.Tags(err)
@@ -109,7 +109,7 @@ func TestErrorTags(t *testing.T) {
109109

110110
func TestErrorMarshalJSON(t *testing.T) {
111111
tag := goerr.NewTag("test")
112-
err := goerr.New("test error",
112+
err := goerr.New("test error",
113113
goerr.Tag(tag),
114114
goerr.Value("key", "value"))
115115

@@ -140,7 +140,7 @@ func TestErrorMarshalJSON(t *testing.T) {
140140

141141
func TestErrorLogValue(t *testing.T) {
142142
err := goerr.New("test error", goerr.Value("key", "value"))
143-
143+
144144
logValue := err.LogValue()
145145
if logValue.Kind() != slog.KindGroup {
146146
t.Error("LogValue should return a group")
@@ -181,9 +181,108 @@ func TestErrorFormat(t *testing.T) {
181181
}
182182
}
183183

184+
func TestErrorFormatWrapped(t *testing.T) {
185+
// Create a chain of wrapped errors with different values
186+
baseErr := goerr.New("base error",
187+
goerr.Value("base_key", "base_value"),
188+
goerr.Value("shared_key", "base_shared")) // This should be overwritten
189+
190+
middleErr := goerr.Wrap(baseErr, "middle error",
191+
goerr.Value("middle_key", "middle_value"),
192+
goerr.Value("shared_key", "middle_shared")) // This overwrites base_shared
193+
194+
topErr := goerr.Wrap(middleErr, "top error",
195+
goerr.Value("top_key", "top_value"),
196+
goerr.Value("shared_key", "top_shared")) // This overwrites middle_shared
197+
198+
// Test %+v format with wrapped errors
199+
detailedResult := fmt.Sprintf("%+v", topErr)
200+
201+
// Should contain the complete error message chain
202+
if !strings.Contains(detailedResult, "top error: middle error: base error") {
203+
t.Error("Detailed format should contain complete error message chain")
204+
}
205+
206+
// Should contain Values section
207+
if !strings.Contains(detailedResult, "Values:") {
208+
t.Error("Detailed format should contain Values section")
209+
}
210+
211+
// Should contain values from base error
212+
if !strings.Contains(detailedResult, "base_key: base_value") {
213+
t.Error("Detailed format should contain values from base error")
214+
}
215+
216+
// Should contain values from middle error
217+
if !strings.Contains(detailedResult, "middle_key: middle_value") {
218+
t.Error("Detailed format should contain values from middle error")
219+
}
220+
221+
// Should contain values from top error
222+
if !strings.Contains(detailedResult, "top_key: top_value") {
223+
t.Error("Detailed format should contain values from top error")
224+
}
225+
226+
// Should show the final value for overwritten key (top-level wins)
227+
if !strings.Contains(detailedResult, "shared_key: top_shared") {
228+
t.Error("Detailed format should show final value for overwritten key")
229+
}
230+
231+
// Should NOT contain overwritten values
232+
if strings.Contains(detailedResult, "base_shared") {
233+
t.Error("Detailed format should not contain overwritten base value")
234+
}
235+
if strings.Contains(detailedResult, "middle_shared") {
236+
t.Error("Detailed format should not contain overwritten middle value")
237+
}
238+
239+
// Verify keys are sorted alphabetically
240+
valuesSection := detailedResult[strings.Index(detailedResult, "Values:"):]
241+
baseKeyPos := strings.Index(valuesSection, "base_key:")
242+
middleKeyPos := strings.Index(valuesSection, "middle_key:")
243+
sharedKeyPos := strings.Index(valuesSection, "shared_key:")
244+
topKeyPos := strings.Index(valuesSection, "top_key:")
245+
246+
// All positions should be found
247+
if baseKeyPos == -1 || middleKeyPos == -1 || sharedKeyPos == -1 || topKeyPos == -1 {
248+
t.Error("All keys should be present in values section")
249+
}
250+
251+
// Keys should appear in alphabetical order
252+
if !(baseKeyPos < middleKeyPos && middleKeyPos < sharedKeyPos && sharedKeyPos < topKeyPos) {
253+
t.Error("Keys should be sorted alphabetically in values section")
254+
}
255+
}
256+
257+
func TestErrorFormatTypedValues(t *testing.T) {
258+
// Test that TypedValues from wrapped errors are also included
259+
// Note: We need to use the typed values functionality here
260+
// Since TypedValues is not directly exposed via options, we test the formatting
261+
// by creating errors that would have typed values through internal mechanisms
262+
263+
baseErr := goerr.New("base error", goerr.Value("regular_key", "regular_value"))
264+
wrappedErr := goerr.Wrap(baseErr, "wrapped error", goerr.Value("wrapped_key", "wrapped_value"))
265+
266+
// Test %+v format includes all values
267+
detailedResult := fmt.Sprintf("%+v", wrappedErr)
268+
269+
// Should contain both regular values
270+
if !strings.Contains(detailedResult, "regular_key: regular_value") {
271+
t.Error("Should contain base error values")
272+
}
273+
if !strings.Contains(detailedResult, "wrapped_key: wrapped_value") {
274+
t.Error("Should contain wrapped error values")
275+
}
276+
277+
// Should contain Values section (since we have regular values)
278+
if !strings.Contains(detailedResult, "Values:") {
279+
t.Error("Should contain Values section")
280+
}
281+
}
282+
184283
func TestErrorStackTrace(t *testing.T) {
185284
err := goerr.New("test error")
186-
285+
187286
stacks := err.Stacks()
188287
if len(stacks) == 0 {
189288
t.Error("Error should have stack trace")
@@ -226,7 +325,7 @@ func TestErrorIs(t *testing.T) {
226325
}
227326

228327
func TestErrorCopy(t *testing.T) {
229-
original := goerr.New("original",
328+
original := goerr.New("original",
230329
goerr.ID("test-id"),
231330
goerr.Value("key", "value"))
232331

@@ -267,7 +366,7 @@ func TestErrorUnstack(t *testing.T) {
267366
// Test UnstackN(1) should have same effect as Unstack
268367
err2 := goerr.New("test error 2")
269368
unstackedN1 := err2.UnstackN(1)
270-
369+
271370
if unstackedN1 != err2 {
272371
t.Error("UnstackN should return the same instance")
273372
}
@@ -300,4 +399,4 @@ func TestPrintable(t *testing.T) {
300399
if len(printable.StackTrace) == 0 {
301400
t.Error("Printable should have stack trace")
302401
}
303-
}
402+
}

errors.go

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"io"
88
"log/slog"
9+
"strconv"
910
"strings"
1011
)
1112

@@ -198,10 +199,12 @@ func (x *Errors) MarshalJSON() ([]byte, error) {
198199
if goErr := Unwrap(err); goErr != nil {
199200
result.Errors[i] = goErr.Printable()
200201
} else if marshaler, ok := err.(json.Marshaler); ok {
201-
data, _ := marshaler.MarshalJSON()
202-
var obj any
203-
_ = json.Unmarshal(data, &obj)
204-
result.Errors[i] = obj
202+
data, marshalErr := marshaler.MarshalJSON()
203+
if marshalErr != nil {
204+
result.Errors[i] = err.Error()
205+
} else {
206+
result.Errors[i] = json.RawMessage(data)
207+
}
205208
} else {
206209
result.Errors[i] = err.Error()
207210
}
@@ -221,12 +224,13 @@ func (x *Errors) LogValue() slog.Value {
221224
}
222225

223226
// Add individual errors
224-
errorAttrs := make([]any, len(x.errs))
227+
errorAttrs := make([]any, 0, len(x.errs)*2)
225228
for i, err := range x.errs {
229+
key := strconv.Itoa(i)
226230
if lv, ok := err.(slog.LogValuer); ok {
227-
errorAttrs[i] = lv.LogValue()
231+
errorAttrs = append(errorAttrs, key, lv.LogValue())
228232
} else {
229-
errorAttrs[i] = err.Error()
233+
errorAttrs = append(errorAttrs, key, err.Error())
230234
}
231235
}
232236
attrs = append(attrs, slog.Group("errors", errorAttrs...))

0 commit comments

Comments
 (0)