Golang Error Resolver With Strategy Design Pattern
In Golang, error handling can quickly become complex and cumbersome when dealing with multiple errors. This can often lead to functions with a high cognitive complexity, which can be challenging to read and maintain. One of the common solutions to this problem is to use the Strategy design pattern.
The Strategy pattern provides a way to encapsulate a family of related algorithms or strategies and make them interchangeable. In the context of error handling, this means defining a set of error-handling strategies that can be applied to different types of errors.
By using the Strategy pattern, we can avoid the use of multiple nested if/else statements to handle different types of errors. Instead, we can define a set of error-handling strategies, each of which is responsible for handling a specific type of error. These strategies can be composed together in a flexible and modular way, making it easy to add or remove error-handling strategies as needed.
Overall, the Strategy pattern can significantly simplify error handling in Golang by reducing the cognitive complexity of functions and making them more maintainable and easier to read.
let's start by defining an interface for Erorr Resolver:
// ErrorResolver interface defines the method to resolve the error
type ErrorResolver interface {
ResolveError(error) error
}
then we will define a set of strategies -different error resolvers-
// NotFoundResolver defines a struct to resolve the error of not found
type NotFoundResolver struct{}
// ResolveError method for NotFoundResolver to resolve not found error
func (r NotFoundResolver) ResolveError(err error) error {
if errors.Is(err, ErrNotFound) {
return fmt.Errorf("resource not found: %w", err)
}
return err
}
// PermissionDeniedResolver defines a struct to resolve the error of permission denied
type PermissionDeniedResolver struct{}
// ResolveError method for PermissionDeniedResolver to resolve permission denied error
func (r PermissionDeniedResolver) ResolveError(err error) error {
if errors.Is(err, ErrPermissionDenied) {
return fmt.Errorf("permission denied: %w", err)
}
return err
}
// OtherErrorResolver defines a struct to resolve other types of errors
type OtherErrorResolver struct{}
// ResolveError method for OtherErrorResolver to resolve other types of errors
func (r OtherErrorResolver) ResolveError(err error) error {
return fmt.Errorf("unexpected error occurred: %w", err)
}
Then we are going to define the Error Resolver Strategy struct:
// ErrorResolverStrategy defines a struct for the strategy pattern to choose the appropriate resolver
type ErrorResolverStrategy struct {
resolvers []ErrorResolver
}
Then we are going to create the NewErrorResolverStrategy function to return a new instance of the ErrorResolverStrategy
// NewErrorResolverStrategy function returns a new instance of the ErrorResolverStrategy struct
func NewErrorResolverStrategy() ErrorResolverStrategy {
return ErrorResolverStrategy{
resolvers: []ErrorResolver{
NotFoundResolver{},
PermissionDeniedResolver{},
OtherErrorResolver{},
},
}
}
Finally, we are going to write the ResolveError which will be responsible for resolving our errors
// ResolveError method for ErrorResolverStrategy to resolve the error using the appropriate resolver
func (s ErrorResolverStrategy) ResolveError(err error) error {
for _, r := range s.resolvers {
err = r.ResolveError(err)
}
return err
}
At this point our Error resolver is complete and we can test its behavior
// define some errors
var (
ErrNotFound = errors.New("not found")
ErrPermissionDenied = errors.New("permission denied")
ErrUnknown = errors.New("unknown error")
)
func main() {
// define some example errors
err1 := fmt.Errorf("some error: %w", ErrNotFound)
err2 := fmt.Errorf("some error: %w", ErrPermissionDenied)
err3 := fmt.Errorf("some error: %w", ErrUnknown)
// create a new error resolver strategy
resolver := NewErrorResolverStrategy()
// resolve the errors using the error resolver strategy
fmt.Println(resolver.ResolveError(err1))
fmt.Println(resolver.ResolveError(err2))
fmt.Println(resolver.ResolveError(err3))
}
output for the above code
unexpected error occurred: resource not found: some error: not found
unexpected error occurred: permission denied: some error: permission denied
unexpected error occurred: some error: unknown error
Program exited.
By demonstrating an alternative method for handling errors, the aim is to showcase an effective and efficient approach to simplifying the handling of multiple errors in Golang. While it is possible to apply a range of optimization and generalization techniques, this approach is particularly noteworthy due to its ability to reduce the cognitive complexity of functions, thereby improving their maintainability and readability.
you can find a complete example here