Error Handling in Go

Learn how to handle errors in Go using the `error` interface and the `defer`, `panic`, and `recover` mechanisms.


Error Handling in Go

Introduction

Error handling is a crucial aspect of writing robust and reliable Go programs. Go provides a simple yet powerful mechanism for managing errors using the error interface and the defer, panic, and recover statements. This document outlines the fundamental concepts of error handling in Go.

The error Interface

In Go, errors are represented by the built-in error interface. This interface is defined as:

type error interface {
    Error() string
}

Any type that implements the Error() string method satisfies the error interface. Functions that can potentially fail typically return an error value as the last return value. If the function succeeds, the error value is typically nil; otherwise, it contains a description of the error.

Example

package main

import (
    "fmt"
    "errors"
)

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

func main() {
    result, err := divide(10, 2)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)

    result, err = divide(10, 0)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    fmt.Println("Result:", result)
} 

Handling Errors with if err != nil

The most common way to handle errors in Go is to check if the error value returned by a function is not nil. If it's not nil, it indicates an error occurred, and you should handle it appropriately, such as logging the error, returning an error up the call stack, or taking corrective action.

Custom Error Types

You can create custom error types by defining a struct or other type that implements the error interface. This allows you to attach more information to the error, such as the specific file or line number where the error occurred.

Example

package main

import (
	"fmt"
)

type MyError struct {
	Message string
	Code    int
}

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

func doSomething(input int) error {
	if input < 0 {
		return &MyError{Message: "Input cannot be negative", Code: 1001}
	}
	return nil
}

func main() {
	err := doSomething(-5)
	if err != nil {
		fmt.Println(err)
	} else {
        fmt.Println("Success!")
    }
} 

defer, panic, and recover

Go provides a mechanism for handling exceptional situations using panic and recover. defer is also heavily used in conjunction with these.

  • defer: A defer statement schedules a function call to be run after the surrounding function returns. This is commonly used to ensure resources are released, such as closing files or database connections, regardless of whether the function returns normally or due to a panic.
  • panic: A panic occurs when the program encounters a critical error that it cannot recover from. When a panic occurs, the program stops executing the current function and begins unwinding the stack, executing any deferred functions along the way.
  • recover: A recover function can be used to regain control after a panic. It can only be called directly within a deferred function. If recover is called within a deferred function that's executing because of a panic, recover stops the panicking sequence by restoring normal execution and returns the error value passed to panic. If recover is called when there is no panic, it returns nil.

Example

package main

import (
	"fmt"
)

func mightPanic() {
	panic("A problem occurred!")
}

func doSomethingSafe() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered from panic:", r)
		}
	}()

	mightPanic()
	fmt.Println("This will not be printed if mightPanic panics.") // Unreachable if panic occurs.
}

func main() {
	doSomethingSafe()
	fmt.Println("Program continues after recovering from panic.")
} 

Important Considerations:

  • Avoid using panic and recover for normal error handling. They are generally reserved for truly exceptional situations where the program cannot continue safely.
  • recover can only be used effectively within a deferred function.
  • Overuse of panic/recover can make code harder to understand and debug.

Error Wrapping (Go 1.13 and later)

Go 1.13 introduced standard support for error wrapping, allowing you to provide more context to errors without losing the original error's information. This is done using the fmt.Errorf function with the %w verb.

Example

package main

import (
	"fmt"
	"errors"
)

func readData() error {
	// Simulate an underlying error
	err := errors.New("failed to read from disk")
	return fmt.Errorf("reading data failed: %w", err) // Wrap the original error
}

func processData() error {
	err := readData()
	if err != nil {
		return fmt.Errorf("processing data failed: %w", err) // Wrap the error further
	}
	return nil
}

func main() {
	err := processData()
	if err != nil {
		fmt.Println("An error occurred:", err)

		// Check if the error chain contains a specific error
		if errors.Is(err, errors.New("failed to read from disk")) {
			fmt.Println("Root cause: Failed to read from disk")
		}
	}
} 

Key benefits of error wrapping:

  • Contextual Information: Provides a stack of errors, making it easier to trace the origin of the problem.
  • Error Inspection: Allows you to inspect the error chain to determine if a specific error is present using errors.Is and errors.As.