Skip to content

Go Error Handling

Go takes a distinct approach to error handling compared to languages that heavily rely on exceptions. Instead of try-catch blocks, Go functions that might fail typically return an additional value of type error alongside their primary result.

error is a built-in interface type defined as:

type error interface {
Error() string
}

Any type that implements the Error() string method satisfies the error interface. A nil error value indicates success, while a non-nil value indicates failure.

By convention, functions that can produce an error return it as their last return value.

func DoSomething(input string) (ResultType, error) {
// ... logic ...
if /* something went wrong */ {
return defaultValue, fmt.Errorf("failed to do something with %s: %w", input, specificError)
}
// ... success ...
return successValue, nil // Return nil error on success
}

The caller of a function that returns an error is responsible for checking the error value immediately after the call.

result, err := DoSomething("some input")
if err != nil {
// Handle the error (e.g., log it, return it, provide default)
log.Printf("Error calling DoSomething: %v", err)
return // Or return err, or handle differently
}
// If err is nil, proceed with using the 'result'
fmt.Println("Success! Result:", result)

This explicit if err != nil check is ubiquitous in Go code and makes error handling visible and mandatory.

Go provides several ways to create error values:

  1. errors.New(message string): Creates a simple error with a static text message. Good for basic, fixed error conditions.
import "errors"
var ErrNotFound = errors.New("resource not found")
func findResource(id string) error {
if /* resource with id doesn't exist */ {
return ErrNotFound
}
return nil
}
  1. fmt.Errorf(format string, args ...any): Creates a formatted error message, similar to fmt.Sprintf. This is very flexible and commonly used.
import "fmt"
func processFile(filename string) error {
_, err := os.Open(filename)
if err != nil {
// Create a more context-rich error message
return fmt.Errorf("failed to open file '%s': %v", filename, err)
}
// ... process file ...
return nil
}

Modern Go encourages error wrapping to provide context without obscuring the original error. The %w verb in fmt.Errorf wraps the underlying error.

import (
"fmt"
"os"
)
func readFileAndProcess(filename string) error {
data, err := os.ReadFile(filename)
if err != nil {
// Wrap the error from os.ReadFile
return fmt.Errorf("error processing file %s: %w", filename, err)
}
// ... process data ...
return nil
}

The errors package provides functions to work with wrapped errors:

err := readFileAndProcess("nonexistent.txt")
if err != nil {
fmt.Println("Original error:", err)
// Check if the underlying error is specifically os.ErrNotExist
if errors.Is(err, os.ErrNotExist) {
fmt.Println("File not found error detected.")
}
// Example with As (if we had a custom error type)
// var customErr *MyCustomErrorType
// if errors.As(err, &customErr) {
// fmt.Println("Custom error code:", customErr.Code)
// }
}

Let’s refine the square root example to use fmt.Errorf and demonstrate the checking pattern.

package main
import (
"fmt"
"math"
)
// Sqrt calculates the square root of a non-negative number.
// It returns an error if the input value is negative.
func Sqrt(value float64) (float64, error) {
if value < 0 {
// Use fmt.Errorf for a more informative error message
return 0, fmt.Errorf("math: cannot calculate square root of negative number %g", value)
}
return math.Sqrt(value), nil // Return nil error on success
}
func main() {
fmt.Println("Attempting Sqrt(-1):")
result1, err1 := Sqrt(-1)
if err1 != nil {
fmt.Println(" Error:", err1)
} else {
fmt.Println(" Result:", result1)
}
fmt.Println("\nAttempting Sqrt(9):")
result2, err2 := Sqrt(9)
if err2 != nil {
fmt.Println(" Error:", err2)
} else {
fmt.Println(" Result:", result2)
}
}

Output:

Attempting Sqrt(-1):
Error: math: cannot calculate square root of negative number -1
Attempting Sqrt(9):
Result: 3

Go’s explicit error handling, while sometimes verbose, leads to more robust and maintainable code by making error paths clear and requiring developers to confront potential failures.