Error Handling in Go

Error handling is a fundamental aspect of writing robust Go programs. Go encourages explicit error checking rather than exceptions, using multiple return values where one is often an error type. This guide covers best practices, common patterns, and advanced techniques for effective error handling.

Basic Error Handling

In Go, functions that can fail typically return an error as the last return value. Always check for errors immediately after calling such functions.

package main

import (
    "fmt"
    "os"
)

func main() {
    data, err := os.ReadFile("hello.txt")
    if err != nil {
        fmt.Println("Error reading file:", err)
        return
    }
    fmt.Println("File content:", string(data))
}

Key Points:

  • Use os.ReadFile instead of the deprecated ioutil.ReadFile.
  • Check err != nil right after the function call.
  • Handle errors gracefully, e.g., by logging or returning early.

Don’t Use Panic for Expected Errors

Panic is for unrecoverable errors, like programming bugs. For expected errors (e.g., file not found), use error returns instead.

// Bad: Using panic for expected errors
data, err := os.ReadFile("hello.txt")
if err != nil {
    panic(err) // Avoid this for recoverable errors
}

// Good: Handle the error
if err != nil {
    fmt.Println("Error:", err)
    // Continue or return
}

When to Use Panic:

  • During initialization if the program cannot continue.
  • In library code for truly unexpected conditions.

Wrapping Errors

Go 1.13 introduced error wrapping with %w verb in fmt.Errorf to preserve the original error for inspection.

import (
    "fmt"
    "errors"
)

func Open(path string) (*DB, error) {
    db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
    if err != nil {
        return nil, fmt.Errorf("opening bolt db: %w", err)
    }
    return &DB{db}, nil
}

Benefits:

  • Use errors.Is() to check if an error matches a specific type.
  • Use errors.As() to extract the underlying error.

Custom Errors

Create custom error types for more context.

type MyError struct {
    Msg  string
    Code int
}

func (e *MyError) Error() string {
    return fmt.Sprintf("error %d: %s", e.Code, e.Msg)
}

func doSomething() error {
    return &MyError{"something went wrong", 42}
}

Defer for Cleanup

Use defer to ensure cleanup happens even if an error occurs.

func processFile(filename string) error {
    file, err := os.Open(filename)
    if err != nil {
        return err
    }
    defer file.Close() // Ensures file is closed

    // Process file...
    return nil
}

Best Practices

  • Fail Fast: Check errors early and return immediately.
  • Don’t Ignore Errors: Always handle or propagate errors.
  • Consistent Error Messages: Use clear, actionable error messages.
  • Logging: Use structured logging for errors in production.
  • Testing: Write tests that cover error paths.

Conclusion

Effective error handling makes Go programs more reliable and maintainable. By following these patterns, you can write code that gracefully handles failures and provides useful debugging information.

Resources