Go Error Handling Tutorial
Error handling in Go is explicit and type-safe, following the language's philosophy of clear and predictable code. Unlike exception-based approaches, Go treats errors as normal return values, encouraging developers to handle them immediately and appropriately.
1. Basic Error Handling
In Go, errors are values that implement the built-in error interface:
type error interface {
Error() string
}
1.1 Creating Simple Errors
The standard library provides several ways to create basic errors:
// Using errors.New()
import "errors"
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Using fmt.Errorf() for formatted errors
func login(username, password string) error {
if username == "" {
return fmt.Errorf("username cannot be empty")
}
if len(password) < 8 {
return fmt.Errorf("password must be at least 8 characters, got %d", len(password))
}
return nil
}
1.2 Handling Errors
The idiomatic way to handle errors in Go is to check them immediately after the function call:
result, err := divide(10, 2)
if err != nil {
fmt.Println("Error:", err)
// Handle the error (return, retry, log, etc.)
return
}
fmt.Println("Result:", result)
// Handling without using the value
if err := login("user", "pass"); err != nil {
fmt.Println("Login failed:", err)
}
2. Custom Error Types
For more sophisticated error handling, you can create custom error types.
2.1 Struct-Based Errors
Define custom error types by implementing the error interface:
type TimeoutError struct {
Operation string
Duration time.Duration
}
func (e *TimeoutError) Error() string {
return fmt.Sprintf("%s timed out after %v", e.Operation, e.Duration)
}
func fetchData() error {
// Simulate timeout
return &TimeoutError{
Operation: "database query",
Duration: 5 * time.Second,
}
}
// Usage
err := fetchData()
if err != nil {
fmt.Println(err) // "database query timed out after 5s"
}
2.2 Type Assertions for Special Handling
Check for specific error types using type assertions:
err := fetchData()
if timeoutErr, ok := err.(*TimeoutError); ok {
fmt.Printf("Timeout occurred during %s\n", timeoutErr.Operation)
// Implement retry logic or other recovery
} else if err != nil {
// Handle other errors
fmt.Println("Error:", err)
}
3. Error Wrapping and Inspection
Go 1.13 introduced error wrapping features for building error chains.
3.1 Wrapping Errors
Use fmt.Errorf with %w verb to wrap errors:
import "os"
func readConfig(path string) ([]byte, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("failed to read config at %s: %w", path, err)
}
return data, nil
}
// Usage
_, err := readConfig("/nonexistent/config.json")
if err != nil {
fmt.Println(err) // "failed to read config at /nonexistent/config.json: open /nonexistent/config.json: no such file or directory"
}
3.2 Inspecting Error Chains
Use errors.Is and errors.As to examine wrapped errors:
// Checking for specific error values
if errors.Is(err, os.ErrNotExist) {
fmt.Println("Config file doesn't exist")
}
// Extracting specific error types
var pathError *os.PathError
if errors.As(err, &pathError) {
fmt.Printf("Operation: %s, Path: %s\n", pathError.Op, pathError.Path)
}
4. Advanced Error Handling Patterns
4.1 Sentinel Errors
Predefined error values for specific error conditions:
var (
ErrUnauthorized = errors.New("unauthorized access")
ErrInvalidInput = errors.New("invalid input")
)
func authenticate(token string) error {
if token == "" {
return ErrUnauthorized
}
// Authentication logic
return nil
}
// Usage
err := authenticate("")
if errors.Is(err, ErrUnauthorized) {
fmt.Println("Please log in first")
}
4.2 Error Aggregation
Combining multiple errors into a single error:
func validateUser(user User) error {
var errs []error
if user.Name == "" {
errs = append(errs, errors.New("name is required"))
}
if user.Age < 18 {
errs = append(errs, errors.New("age must be 18+"))
}
if len(errs) > 0 {
return fmt.Errorf("validation failed: %v", errs)
}
return nil
}
5. Context and Error Handling
5.1 Errors with Context
Adding contextual information to errors:
type ContextError struct {
Context string
Err error
}
func (e *ContextError) Error() string {
return fmt.Sprintf("%s: %v", e.Context, e.Err)
}
func (e *ContextError) Unwrap() error {
return e.Err
}
func processFile(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
return &ContextError{
Context: fmt.Sprintf("processing %s", filename),
Err: err,
}
}
// Process data
return nil
}
5.2 Timeout and Cancellation
Handling context cancellation in errors:
func longOperation(ctx context.Context) error {
select {
case <-time.After(5 * time.Second):
return nil // Operation completed
case <-ctx.Done():
return ctx.Err() // Returns context.Canceled or context.DeadlineExceeded
}
}
// Usage
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := longOperation(ctx)
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("Operation timed out")
}
6. Error Handling Best Practices
6.1 When to Handle vs When to Return
Guidelines for error propagation:
// Good: Handle errors you can recover from
func parseNumber(s string) (int, error) {
n, err := strconv.Atoi(s)
if err != nil {
// Can't recover from this - return the error
return 0, fmt.Errorf("invalid number: %w", err)
}
return n, nil
}
// Good: Add context when returning errors
func loadUser(id string) (*User, error) {
data, err := db.Query("SELECT * FROM users WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("loading user %s: %w", id, err)
}
// ...
}
// Bad: Don't ignore errors
file, _ := os.Open("file.txt") // WRONG - always handle errors
6.2 Logging Strategies
Effective error logging patterns:
// Good: Log with context
if err := processOrder(order); err != nil {
log.Printf("Failed to process order %s: %v", order.ID, err)
// Return a cleaned error to the caller
return fmt.Errorf("order processing failed")
}
// Good: Structured logging
if err := sendEmail(user); err != nil {
log.WithFields(log.Fields{
"user": user.ID,
"email": user.Email,
}).Error("Failed to send email")
}
7. Recovery and Panics
7.1 Handling Panics
Recovering from unexpected errors:
func safeOperation() (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("recovered from panic: %v", r)
}
}()
// Potentially panicking code
return nil
}
// Usage
err := safeOperation()
if err != nil {
fmt.Println("Operation failed:", err)
}
7.2 When to Use Panic
Appropriate uses of panic/recover:
// Appropriate: Programmer errors during development
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(fmt.Sprintf("invalid number in mustParse: %s", s))
}
return n
}
// Appropriate: Initialization failures
func init() {
if os.Getenv("DB_URL") == "" {
panic("DB_URL environment variable not set")
}
}
// Inappropriate: Don't use panic for normal error conditions
func getUser(id string) (User, error) {
// Return error instead of panic when user not found
}