Error handling is a crucial part of building robust and reliable applications. Go (Golang) has a built-in error interface that serves as the foundation for handling errors. To build your own custom error types, you need to understand how Go interfaces work and how errors should be constructed following Go’s idioms.
Understanding the Go `error` Interface
The Go error interface is quite simple. It consists of just one method:
type error interface {
Error() string
}
The Error() method returns the error message as a string. This simplicity allows a great deal of flexibility; you can create various error types that implement this interface.
Basic Example: Implementing a Custom Error
Let’s start with a basic example. Imagine a scenario where a function needs to return a specific type of error if a numeric condition isn’t met:
package main
import (
"fmt"
)
// Define a custom error type.
type SimpleError struct {
ErrMessage string
}
// Implement the Error method for SimpleError.
func (e *SimpleError) Error() string {
return e.ErrMessage
}
}
// A function demonstrating how to return a SimpleError
func FailWhenNegative(num int) error {
if num < 0 {
return &SimpleError{ErrMessage: "Number cannot be negative"}
}
return nil
}
func main() {
// Attempt an operation that could fail.
if err := FailWhenNegative(-1); err != nil {
fmt.Println(err)
}
}
In this basic example, we defined SimpleError with an error message field, and implemented the Error() method to satisfy the error interface. The FailWhenNegative function checks a condition and returns an error if the condition isn’t met.
Intermediate Example: Adding More Functionality to Your Custom Error
Now, let’s make the custom error more informative by adding additional context such as codes or underlying errors:
package main
import (
"fmt"
)
// CustomError holds an error message, code, and optionally another error.
type CustomError struct {
ErrMessage string
Code int
Err error
}
// Implement the Error method for CustomError.
func (e *CustomError) Error() string {
if e.Err != nil {
return fmt.Sprintf("Error %d: %s - Root cause: %v", e.Code, e.ErrMessage, e.Err)
}
return fmt.Sprintf("Error %d: %s", e.Code, e.ErrMessage)
}
// Function with more complex error.
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, &CustomError{ErrMessage: "Division by zero", Code: 400, Err: fmt.Errorf("b was %d", b)}
}
return a / b, nil
}
func main() {
// An operation that can fail
if result, err := Divide(4, 0); err != nil {
fmt.Println(err)
} else {
fmt.Println("The result is", result)
}
}
In this example, CustomError provides an error code and can optionally hold another error instance to trace the root of an issue more explicitly. This is helpful when handling errors in nested operations.
Advanced Example: Wrapping and Unwrapping Errors
Go 1.13 introduced error wrapping and unwrapping. It is particularly useful for creating nested errors with straightforward stack tracing:
package main
import (
"errors"
"fmt"
)
// WrapAndDivide to wrap low-level errors
func WrapAndDivide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func HandleDivide(a, b int) (int, error) {
result, err := WrapAndDivide(a, b)
if err != nil {
// Wrap the error with additional context
return 0, fmt.Errorf("HandleDivide failed: %w", err)
}
return result, nil
}
func main() {
if result, err := HandleDivide(10, 0); err != nil {
// Unwrap the error
if errors.Is(err, errors.New("division by zero")) {
fmt.Println("Cannot perform division due to zero denominator.")
}
fmt.Println(err)
} else {
fmt.Println("The result is", result)
}
}
This example shows error wrapping with the %w format verb, which allows you to add context to an error. The errors.Is() function permits checking if an error matches a specific one within a stack of wrapped errors.
Conclusion
Understanding and leveraging customized error types in Go can significantly improve error handling in your applications. By developing error types that implement the error interface, you tailor the error reporting to meet your application’s specific needs. This ensures that the operations that use these custom errors can handle issues effectively at every abstraction level of your program stack.