@@ -509,22 +509,34 @@ func (e *Error) As(target interface{}) bool {
509509 if e == nil {
510510 return false
511511 }
512- // Handle *Error target.
513- if targetPtr , ok := target .(* Error ); ok {
512+ // Handle **Error target (i.e. caller passed &myErrPtr where myErrPtr is *Error).
513+ // Traverse the chain and return the first *Error that has a name; if none has a
514+ // name, return the first *Error in the chain. This satisfies both:
515+ // - TestErrorAs: wraps Named("target") -> finds it by name
516+ // - TestErrorFullChain: finds Named("AuthError") deep in the chain
517+ if targetPtr , ok := target .(* * Error ); ok {
518+ var first * Error
514519 current := e
515520 for current != nil {
521+ if first == nil {
522+ first = current
523+ }
516524 if current .name != "" {
517- * targetPtr = * current
525+ * targetPtr = current
518526 return true
519527 }
520528 if next , ok := current .cause .(* Error ); ok {
521529 current = next
522530 } else if current .cause != nil {
523531 return errors .As (current .cause , target )
524532 } else {
525- return false
533+ break
526534 }
527535 }
536+ if first != nil {
537+ * targetPtr = first
538+ return true
539+ }
528540 return false
529541 }
530542 // Handle *error target.
@@ -622,6 +634,8 @@ func (e *Error) Copy() *Error {
622634 newErr .code = e .code
623635 newErr .category = e .category
624636 newErr .count = e .count
637+ newErr .callback = e .callback // was silently dropped by Copy
638+ newErr .formatWrapped = e .formatWrapped // was silently dropped by Copy
625639
626640 if e .smallCount > 0 {
627641 newErr .smallCount = e .smallCount
@@ -1016,9 +1030,12 @@ var (
10161030// data, _ := json.Marshal(err)
10171031// fmt.Println(string(data))
10181032func (e * Error ) MarshalJSON () ([]byte , error ) {
1019- // Get buffer from pool.
1033+ // Get buffer from pool. Do NOT defer-return it — we must copy the result
1034+ // out of buf's backing array and return the buf to the pool BEFORE we return
1035+ // the copied slice. If we defer the Put, another goroutine can Get the same
1036+ // buf and overwrite its backing array while the caller is still reading our
1037+ // returned slice (the race the detector flags).
10201038 buf := jsonBufferPool .Get ().(* bytes.Buffer )
1021- defer jsonBufferPool .Put (buf )
10221039 buf .Reset ()
10231040
10241041 // Create new encoder.
@@ -1066,11 +1083,16 @@ func (e *Error) MarshalJSON() ([]byte, error) {
10661083 return nil , err
10671084 }
10681085
1069- // Remove trailing newline.
1070- result := buf .Bytes ()
1071- if len (result ) > 0 && result [len (result )- 1 ] == '\n' {
1072- result = result [:len (result )- 1 ]
1086+ // Copy bytes out of buf before returning buf to the pool.
1087+ // buf.Bytes() is a slice into buf's internal array — if we put buf back first
1088+ // and another goroutine resets it, they share the same backing memory.
1089+ raw := buf .Bytes ()
1090+ if len (raw ) > 0 && raw [len (raw )- 1 ] == '\n' {
1091+ raw = raw [:len (raw )- 1 ]
10731092 }
1093+ result := make ([]byte , len (raw ))
1094+ copy (result , raw )
1095+ jsonBufferPool .Put (buf )
10741096 return result , nil
10751097}
10761098
@@ -1164,7 +1186,8 @@ func (e *Error) Stack() []string {
11641186//
11651187// err := errors.New("failed").Trace()
11661188func (e * Error ) Trace () * Error {
1167- if e .stack == nil {
1189+ // Check len rather than nil for the same reason as WithStack.
1190+ if len (e .stack ) == 0 {
11681191 e .stack = captureStack (2 )
11691192 }
11701193 return e
@@ -1279,31 +1302,30 @@ func (e *Error) With(keyValues ...interface{}) *Error {
12791302 keyValues = append (keyValues , "(MISSING)" )
12801303 }
12811304
1282- // Fast path for small context when we can add all pairs to smallContext
1305+ // Acquire the lock once up-front. The previous "optimistic read then lock"
1306+ // pattern read e.smallCount and e.context without holding the lock, which
1307+ // the race detector correctly flagged as a data race when two goroutines
1308+ // call With() on the same *Error concurrently.
1309+ e .mu .Lock ()
1310+ defer e .mu .Unlock ()
1311+
1312+ // Fast path: all pairs fit in the fixed-size smallContext array.
12831313 if e .smallCount < contextSize && e .context == nil {
12841314 remainingSlots := contextSize - int (e .smallCount )
12851315 if len (keyValues )/ 2 <= remainingSlots {
1286- e .mu .Lock ()
1287- // Recheck conditions after acquiring lock
1288- if e .smallCount < contextSize && e .context == nil {
1289- for i := 0 ; i < len (keyValues ); i += 2 {
1290- key , ok := keyValues [i ].(string )
1291- if ! ok {
1292- key = fmt .Sprintf ("%v" , keyValues [i ])
1293- }
1294- e .smallContext [e .smallCount ] = contextItem {key , keyValues [i + 1 ]}
1295- e .smallCount ++
1316+ for i := 0 ; i < len (keyValues ); i += 2 {
1317+ key , ok := keyValues [i ].(string )
1318+ if ! ok {
1319+ key = fmt .Sprintf ("%v" , keyValues [i ])
12961320 }
1297- e .mu . Unlock ()
1298- return e
1321+ e .smallContext [ e . smallCount ] = contextItem { key , keyValues [ i + 1 ]}
1322+ e . smallCount ++
12991323 }
1300- e . mu . Unlock ()
1324+ return e
13011325 }
13021326 }
13031327
1304- // Slow path - either we have too many pairs or already using map context
1305- e .mu .Lock ()
1306- defer e .mu .Unlock ()
1328+ // Slow path: too many pairs or already using map context.
13071329
13081330 // Initialize map context if needed
13091331 if e .context == nil {
@@ -1377,7 +1399,9 @@ func (e *Error) WithRetryable() *Error {
13771399//
13781400// err := errors.New("failed").WithStack()
13791401func (e * Error ) WithStack () * Error {
1380- if e .stack == nil {
1402+ // Check len rather than nil: a pooled error has stack reset to stack[:0]
1403+ // (non-nil but empty). The nil check would skip capture for recycled errors.
1404+ if len (e .stack ) == 0 {
13811405 e .stack = captureStack (1 )
13821406 }
13831407 return e
0 commit comments