mirror of
https://github.com/aykhans/go-utils.git
synced 2025-10-15 18:25:57 +00:00
feat: add 'HandleErrorOrDefault' with fallback handler
This commit is contained in:
23
README.md
23
README.md
@@ -121,6 +121,29 @@ result := errors.HandleErrorOrDie(err,
|
|||||||
) // Panics if err doesn't match any handler
|
) // Panics if err doesn't match any handler
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**HandleErrorOrDefault** - Handle errors with a default fallback
|
||||||
|
```go
|
||||||
|
result := errors.HandleErrorOrDefault(err,
|
||||||
|
func(e error) error {
|
||||||
|
// Default handler for unmatched errors
|
||||||
|
return fmt.Errorf("unexpected error: %w", e)
|
||||||
|
},
|
||||||
|
errors.OnSentinelError(context.Canceled, func(e error) error {
|
||||||
|
return fmt.Errorf("operation canceled")
|
||||||
|
}),
|
||||||
|
errors.OnCustomError(func(e *ValidationError) error {
|
||||||
|
return fmt.Errorf("validation failed: %w", e)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pass nil to suppress unmatched errors
|
||||||
|
result := errors.HandleErrorOrDefault(err, nil,
|
||||||
|
errors.OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handled")
|
||||||
|
}),
|
||||||
|
) // Returns nil for unmatched errors
|
||||||
|
```
|
||||||
|
|
||||||
**OnSentinelError** - Create matcher for sentinel errors (like `io.EOF`)
|
**OnSentinelError** - Create matcher for sentinel errors (like `io.EOF`)
|
||||||
```go
|
```go
|
||||||
matcher := errors.OnSentinelError(io.EOF, func(e error) error {
|
matcher := errors.OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
@@ -14,7 +14,7 @@ tasks:
|
|||||||
|
|
||||||
tidy: go mod tidy {{.CLI_ARGS}}
|
tidy: go mod tidy {{.CLI_ARGS}}
|
||||||
|
|
||||||
test: go test ./... {{.CLI_ARGS}}
|
test: go test -race ./... {{.CLI_ARGS}}
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
desc: Run linters
|
desc: Run linters
|
||||||
|
@@ -81,6 +81,45 @@ func HandleErrorOrDie(err error, matchers ...ErrorMatcher) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandleErrorOrDefault processes an error against a list of matchers and executes the appropriate handler.
|
||||||
|
// If a matching handler is found, it returns the handler's result.
|
||||||
|
// If no matcher matches the error, it executes the default handler (dft) and returns its result.
|
||||||
|
// If dft is nil, unmatched errors return nil (effectively suppressing the error).
|
||||||
|
// This function is useful when you want to handle specific error cases explicitly
|
||||||
|
// while providing a fallback handler for all other errors.
|
||||||
|
//
|
||||||
|
// Example:
|
||||||
|
//
|
||||||
|
// result := HandleErrorOrDefault(err,
|
||||||
|
// func(e error) error {
|
||||||
|
// // Default handler for unmatched errors
|
||||||
|
// return fmt.Errorf("unexpected error: %w", e)
|
||||||
|
// },
|
||||||
|
// OnSentinelError(context.Canceled, func(e error) error {
|
||||||
|
// return fmt.Errorf("operation canceled")
|
||||||
|
// }),
|
||||||
|
// OnCustomError(func(e *ValidationError) error {
|
||||||
|
// return fmt.Errorf("validation failed: %w", e)
|
||||||
|
// }),
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Suppress unmatched errors by passing nil as default handler
|
||||||
|
// result := HandleErrorOrDefault(err, nil,
|
||||||
|
// OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
// return errors.New("EOF handled")
|
||||||
|
// }),
|
||||||
|
// ) // Returns nil for unmatched errors
|
||||||
|
func HandleErrorOrDefault(err error, dft ErrorHandler, matchers ...ErrorMatcher) error {
|
||||||
|
ok, err := HandleError(err, matchers...)
|
||||||
|
if !ok {
|
||||||
|
if dft == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return dft(err)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// OnSentinelError creates an ErrorMatcher for sentinel errors.
|
// OnSentinelError creates an ErrorMatcher for sentinel errors.
|
||||||
// Sentinel errors are predefined error values that are compared using errors.Is.
|
// Sentinel errors are predefined error values that are compared using errors.Is.
|
||||||
//
|
//
|
||||||
|
@@ -251,6 +251,288 @@ func TestHandleErrorOrDie(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandleErrorOrDefault(t *testing.T) {
|
||||||
|
t.Run("HandleErrorOrDefault with nil error", func(t *testing.T) {
|
||||||
|
defaultCalled := false
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
nil,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, result)
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with matched error", func(t *testing.T) {
|
||||||
|
err := io.EOF
|
||||||
|
defaultCalled := false
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
err,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("handled EOF")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
require.EqualError(t, result, "handled EOF")
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with unmatched error calls default", func(t *testing.T) {
|
||||||
|
err := errors.New("unmatched error")
|
||||||
|
defaultCalled := false
|
||||||
|
var capturedErr error
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
err,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
capturedErr = e
|
||||||
|
return fmt.Errorf("default: %w", e)
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, defaultCalled)
|
||||||
|
assert.Equal(t, err, capturedErr)
|
||||||
|
assert.EqualError(t, result, "default: unmatched error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with custom error match", func(t *testing.T) {
|
||||||
|
customErr := &CustomError{Code: 404, Message: "not found"}
|
||||||
|
defaultCalled := false
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
customErr,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
OnCustomError(func(e *CustomError) error {
|
||||||
|
return fmt.Errorf("custom handler: code=%d", e.Code)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
assert.EqualError(t, result, "custom handler: code=404")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with custom error no match", func(t *testing.T) {
|
||||||
|
customErr := &CustomError{Code: 500, Message: "server error"}
|
||||||
|
defaultCalled := false
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
customErr,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return fmt.Errorf("default for custom: %v", e)
|
||||||
|
},
|
||||||
|
OnCustomError(func(e *ValidationError) error {
|
||||||
|
return errors.New("validation handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, defaultCalled)
|
||||||
|
assert.EqualError(t, result, "default for custom: custom error 500: server error")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with multiple matchers", func(t *testing.T) {
|
||||||
|
validationErr := &ValidationError{Field: "email", Value: "invalid"}
|
||||||
|
defaultCalled := false
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
validationErr,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
OnCustomError(func(e *CustomError) error {
|
||||||
|
return errors.New("custom handler")
|
||||||
|
}),
|
||||||
|
OnCustomError(func(e *ValidationError) error {
|
||||||
|
return fmt.Errorf("validation handler: field=%s", e.Field)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
assert.EqualError(t, result, "validation handler: field=email")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault default returns nil", func(t *testing.T) {
|
||||||
|
err := errors.New("some error")
|
||||||
|
defaultCalled := false
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
err,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return nil // Default handler suppresses error
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.True(t, defaultCalled)
|
||||||
|
assert.NoError(t, result)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with wrapped error", func(t *testing.T) {
|
||||||
|
wrappedErr := fmt.Errorf("wrapped: %w", io.EOF)
|
||||||
|
defaultCalled := false
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
wrappedErr,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("handled wrapped EOF")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
assert.EqualError(t, result, "handled wrapped EOF")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with context errors", func(t *testing.T) {
|
||||||
|
// Test matched context.Canceled
|
||||||
|
defaultCalled := false
|
||||||
|
result1 := HandleErrorOrDefault(
|
||||||
|
context.Canceled,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default handler")
|
||||||
|
},
|
||||||
|
OnSentinelError(context.Canceled, func(e error) error {
|
||||||
|
return errors.New("operation canceled")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.False(t, defaultCalled)
|
||||||
|
require.EqualError(t, result1, "operation canceled")
|
||||||
|
|
||||||
|
// Test unmatched context.DeadlineExceeded
|
||||||
|
defaultCalled = false
|
||||||
|
result2 := HandleErrorOrDefault(
|
||||||
|
context.DeadlineExceeded,
|
||||||
|
func(e error) error {
|
||||||
|
defaultCalled = true
|
||||||
|
return errors.New("default: deadline exceeded")
|
||||||
|
},
|
||||||
|
OnSentinelError(context.Canceled, func(e error) error {
|
||||||
|
return errors.New("operation canceled")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
assert.True(t, defaultCalled)
|
||||||
|
assert.EqualError(t, result2, "default: deadline exceeded")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault preserves error in default handler", func(t *testing.T) {
|
||||||
|
originalErr := &CustomError{Code: 403, Message: "forbidden"}
|
||||||
|
var capturedErr error
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
originalErr,
|
||||||
|
func(e error) error {
|
||||||
|
capturedErr = e
|
||||||
|
return fmt.Errorf("default handled: %w", e)
|
||||||
|
},
|
||||||
|
OnCustomError(func(e *ValidationError) error {
|
||||||
|
return errors.New("validation handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, originalErr, capturedErr)
|
||||||
|
assert.EqualError(t, result, "default handled: custom error 403: forbidden")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault default handler can access error details", func(t *testing.T) {
|
||||||
|
customErr := &CustomError{Code: 500, Message: "internal error"}
|
||||||
|
wrappedErr := fmt.Errorf("operation failed: %w", customErr)
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
wrappedErr,
|
||||||
|
func(e error) error {
|
||||||
|
// Default handler can still use errors.As to extract details
|
||||||
|
var ce *CustomError
|
||||||
|
if errors.As(e, &ce) {
|
||||||
|
return fmt.Errorf("default: found code %d", ce.Code)
|
||||||
|
}
|
||||||
|
return errors.New("default: unknown error")
|
||||||
|
},
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.EqualError(t, result, "default: found code 500")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with nil default handler and no match", func(t *testing.T) {
|
||||||
|
err := errors.New("unmatched error")
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
err,
|
||||||
|
nil, // nil default handler
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, result) // Should return nil for unmatched errors
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with nil default handler and match", func(t *testing.T) {
|
||||||
|
err := io.EOF
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
err,
|
||||||
|
nil, // nil default handler (should not be called)
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handled")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.EqualError(t, result, "EOF handled") // Should use the matcher, not default
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with nil default handler and nil error", func(t *testing.T) {
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
nil,
|
||||||
|
nil, // nil default handler
|
||||||
|
OnSentinelError(io.EOF, func(e error) error {
|
||||||
|
return errors.New("EOF handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, result) // nil error should return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("HandleErrorOrDefault with nil default handler suppresses custom errors", func(t *testing.T) {
|
||||||
|
customErr := &CustomError{Code: 404, Message: "not found"}
|
||||||
|
|
||||||
|
result := HandleErrorOrDefault(
|
||||||
|
customErr,
|
||||||
|
nil, // nil default handler suppresses unmatched errors
|
||||||
|
OnCustomError(func(e *ValidationError) error {
|
||||||
|
return errors.New("validation handler")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.NoError(t, result) // Should return nil since no matcher matches
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestOnSentinelError(t *testing.T) {
|
func TestOnSentinelError(t *testing.T) {
|
||||||
t.Run("OnSentinelError creates proper matcher", func(t *testing.T) {
|
t.Run("OnSentinelError creates proper matcher", func(t *testing.T) {
|
||||||
handler := func(e error) error { return e }
|
handler := func(e error) error { return e }
|
||||||
|
Reference in New Issue
Block a user