Introduction
Go 1.13 introduced errors.Is and errors.As for error inspection. A subtle bug occurs when errors.As returns true but the target variable receives a nil pointer, or when a custom error type's Unwrap() method returns nil, causing nil pointer dereference when the caller tries to access the unwrapped error's fields.
Symptoms
panic: runtime error: invalid memory address or nil pointer dereference- Stack trace points to line accessing fields after
errors.As errors.Is(err, target)returns true buterris actually nil- Custom error types with nil embedded errors
```go type APIError struct { StatusCode int Message string Err error }
func (e *APIError) Unwrap() error { return e.Err }
// In caller: var apiErr *APIError if errors.As(err, &apiErr) { fmt.Println(apiErr.StatusCode) // PANIC if apiErr is nil! } ```
Common Causes
- Custom error types with nil
Unwrap()return errors.Aswith pointer-to-pointer that resolves to nil- Error chain where intermediate errors are nil
- Third-party libraries returning non-standard error types
- Wrapping nil errors:
fmt.Errorf("context: %w", nil)produces non-nil error
Step-by-Step Fix
- 1.Always check for nil after errors.As:
- 2.```go
- 3.var apiErr *APIError
- 4.if errors.As(err, &apiErr) && apiErr != nil {
- 5.// Safe to access apiErr fields
- 6.log.Printf("API error %d: %s", apiErr.StatusCode, apiErr.Message)
- 7.}
- 8.
` - 9.Fix custom error Unwrap to not return nil:
- 10.```go
- 11.// WRONG - Unwrap can return nil
- 12.type MyError struct {
- 13.cause error
- 14.}
func (e *MyError) Unwrap() error { return e.cause // May be nil! }
// CORRECT - only implement Unwrap if cause is non-nil func (e *MyError) Unwrap() error { if e.cause == nil { return nil // errors.Is/As handle this correctly } return e.cause }
// Better: use fmt.Errorf for wrapping func (e *MyError) Unwrap() error { return e.cause }
// And never create with nil cause: func NewMyError(msg string) error { return &MyError{Message: msg} // cause defaults to nil, Unwrap handles it } ```
- 1.Avoid wrapping nil errors:
- 2.```go
- 3.// WRONG - creates a non-nil error wrapping nil
- 4.func doSomething() error {
- 5.err := mightReturnNil()
- 6.if err != nil {
- 7.return fmt.Errorf("failed: %w", err)
- 8.}
- 9.return nil // This is fine
- 10.}
// If err is nil and you return fmt.Errorf("failed: %w", nil), // the returned error is non-nil but unwraps to nil ```
- 1.Use safe unwrap helper:
- 2.```go
- 3.func UnwrapAs[T error](err error) (T, bool) {
- 4.var target T
- 5.if errors.As(err, &target) {
- 6.var zero T
- 7.if target != zero { // Type-safe nil check
- 8.return target, true
- 9.}
- 10.}
- 11.return zero, false
- 12.}
// Usage if apiErr, ok := UnwrapAs[*APIError](err); ok { fmt.Println(apiErr.StatusCode) // Safe } ```
Prevention
- Always add nil check after
errors.As - Never return
fmt.Errorf("msg: %w", nil)- check before wrapping - Test error chains with
errors.Is(err, nil)- should always return false - Use
go vetto catch common nil pointer issues - Write table-driven tests that include nil error cases
- Consider using
github.com/pkg/errorsfor stack traces with proper nil handling