Go Gopher How to Go

Go handles errors explicitly through return values rather than exceptions. Errors are values that implement the error interface, making error handling straightforward and visible in your code.

💡 Key Points

  • Errors are values, not exceptions
  • Functions return errors as the last return value
  • Always check errors immediately after function calls
  • The error interface has one method: Error() string
  • Use errors.New() to create simple errors
  • Use fmt.Errorf() for formatted error messages
  • Wrap errors with %w to preserve error chain
  • Use errors.Is() and errors.As() for error checking

Basic Error Handling

Functions return errors that must be explicitly checked:

package main

import (
    "errors"
    "fmt"
)

func divide(a, b float64) (float64, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func main() {
    // Proper error checking
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
    
    // Error case
    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
}
Output:
Result: 5
Error: division by zero

Custom Errors

Create custom error types for more context:

package main

import "fmt"

// Custom error type
type ValidationError struct {
    Field   string
    Message string
}

// Implement error interface
func (e *ValidationError) Error() string {
    return fmt.Sprintf("validation error on '%s': %s", e.Field, e.Message)
}

func validateAge(age int) error {
    if age < 0 {
        return &ValidationError{
            Field:   "age",
            Message: "cannot be negative",
        }
    }
    if age > 150 {
        return &ValidationError{
            Field:   "age",
            Message: "unrealistic value",
        }
    }
    return nil
}

func main() {
    // Valid age
    if err := validateAge(30); err != nil {
        fmt.Println("Error:", err)
    } else {
        fmt.Println("Age is valid")
    }
    
    // Invalid age
    if err := validateAge(-5); err != nil {
        fmt.Println("Error:", err)
    }
    
    // Check specific error type
    err := validateAge(200)
    if validErr, ok := err.(*ValidationError); ok {
        fmt.Printf("Field '%s' failed: %s\n", validErr.Field, validErr.Message)
    }
}
Output:
Age is valid
Error: validation error on 'age': cannot be negative
Field 'age' failed: unrealistic value

Error Wrapping and Unwrapping

Wrap errors to add context while preserving the error chain:

package main

import (
    "errors"
    "fmt"
    "os"
)

var ErrNotFound = errors.New("not found")

func readConfig(filename string) error {
    _, err := os.Open(filename)
    if err != nil {
        // Wrap error with context using %w
        return fmt.Errorf("failed to read config: %w", err)
    }
    return nil
}

func initialize() error {
    err := readConfig("config.json")
    if err != nil {
        return fmt.Errorf("initialization failed: %w", err)
    }
    return nil
}

func main() {
    err := initialize()
    if err != nil {
        fmt.Println("Error:", err)
        
        // Check if error chain contains specific error
        if errors.Is(err, os.ErrNotExist) {
            fmt.Println("Config file does not exist")
        }
        
        // Unwrap to get underlying error
        fmt.Println("Unwrapped:", errors.Unwrap(err))
    }
}
Output:
Error: initialization failed: failed to read config: open config.json: no such file or directory
Config file does not exist
Unwrapped: failed to read config: open config.json: no such file or directory

Error Checking Functions

FunctionSignaturePurpose
errors.Newfunc New(text string) errorCreate simple error
fmt.Errorffunc Errorf(format string, a ...any) errorCreate formatted error
errors.Isfunc Is(err, target error) boolCheck if error matches target
errors.Asfunc As(err error, target any) boolCheck if error is of type
errors.Unwrapfunc Unwrap(err error) errorGet wrapped error
errors.Joinfunc Join(errs ...error) errorCombine multiple errors (Go 1.20+)

Error Handling Patterns

PatternCodeUse Case
Immediate checkif err != nil { return err }Most common pattern
Inline checkif err := fn(); err != nil { }Limit error scope
Wrap and returnreturn fmt.Errorf("context: %w", err)Add context to error
Sentinel errorvar ErrNotFound = errors.New("not found")Define known errors
Error variablevar err errorReuse across calls
Named returnfunc fn() (err error) { defer ... }Clean up on error

Panic and Recover

FunctionDescriptionWhen to Use
panic(v)Stop normal executionUnrecoverable errors, programming bugs
recover()Regain control after panicIn deferred functions to catch panics
deferExecute before function returnsCleanup, recover from panics

Error Best Practices

PracticeDescriptionExample
Check all errorsNever ignore error valuesAlways use if err != nil
Add contextWrap errors with meaningful messagesfmt.Errorf("failed to X: %w", err)
Use %w for wrappingPreserve error chain for Is/Asfmt.Errorf("context: %w", err)
Return earlyHandle errors immediatelyif err != nil { return err }
Descriptive messagesError messages should be clear"failed to connect to database"
Lowercase messagesError strings start lowercaseerrors.New("connection failed")
No punctuationDon't end with period"not found" not "not found."
Define sentinel errorsUse variables for known errorsvar ErrNotFound = ...
Custom types for contextAdd fields to error structsInclude codes, fields, etc.
Avoid panicUse panic only for bugsReturn errors instead

Common Error Patterns

PatternExampleUse Case
Error as last returnfunc Open() (*File, error)Standard Go convention
Boolean ok patternv, ok := m[key]Map lookups, type assertions
Defer cleanupdefer file.Close()Ensure cleanup happens
Error grouperrgroup.GroupCollect errors from goroutines
Multi-errorerrors.Join(err1, err2)Multiple errors at once