Skip to main content

Go Error Interface

The error interface in Go is a fundamental part of the language's error handling system. It provides a simple, consistent way to represent and handle errors throughout Go programs. Understanding the error interface, how to create errors, and how to handle them is essential for writing robust Go applications. This comprehensive guide will teach you everything you need to know about Go's error interface and basic error handling techniques.

Understanding the Error Interface

What Is the Error Interface?

The error interface in Go is a built-in interface that defines a single method for error handling:

type error interface {
Error() string
}

This simple interface provides:

  • Consistency - All errors implement the same interface
  • Simplicity - Only one method to implement
  • Flexibility - Can be implemented by any type
  • Compatibility - Works with all error handling patterns

The Error Interface Characteristics

Single Method Interface

The error interface has only one method: Error() string.

Built-in Interface

The error interface is built into the Go language.

Implicit Implementation

Any type that implements Error() string automatically satisfies the error interface.

Creating Errors

Using the errors Package

The errors.New() Function

Creates a simple error with a message.

Basic Error Creation

Simple and direct error creation methods.

package main

import (
"errors"
"fmt"
)

func main() {
// Creating errors examples
fmt.Println("Creating errors examples:")

// Basic error creation with errors.New()
err1 := errors.New("something went wrong")
fmt.Printf("Error 1: %v\n", err1)
fmt.Printf("Error 1 string: %s\n", err1.Error())
// Output:
// Error 1: something went wrong
// Error 1 string: something went wrong

// Error creation with different messages
err2 := errors.New("file not found")
err3 := errors.New("permission denied")
err4 := errors.New("network timeout")

fmt.Printf("Error 2: %v\n", err2)
fmt.Printf("Error 3: %v\n", err3)
fmt.Printf("Error 4: %v\n", err4)
// Output:
// Error 2: file not found
// Error 3: permission denied
// Error 4: network timeout

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

// Use the function
result, err := divide(10, 2)
if err != nil {
fmt.Printf("Division error: %v\n", err)
} else {
fmt.Printf("Division result: %.2f\n", result)
}
// Output: Division result: 5.00

result, err = divide(10, 0)
if err != nil {
fmt.Printf("Division error: %v\n", err)
} else {
fmt.Printf("Division result: %.2f\n", result)
}
// Output: Division error: division by zero

// Error creation with validation
func validateAge(age int) error {
if age < 0 {
return errors.New("age cannot be negative")
}
if age > 150 {
return errors.New("age cannot exceed 150")
}
return nil
}

// Test validation
ages := []int{25, -5, 200, 30}
for _, age := range ages {
err := validateAge(age)
if err != nil {
fmt.Printf("Age %d: %v\n", age, err)
} else {
fmt.Printf("Age %d: valid\n", age)
}
}
// Output:
// Age 25: valid
// Age -5: age cannot be negative
// Age 200: age cannot exceed 150
// Age 30: valid
}

Using the fmt Package for Error Creation

The fmt.Errorf() Function

Creates formatted errors with additional context.

Error Formatting

Creating errors with dynamic content and context.

package main

import (
"fmt"
"strconv"
)

func main() {
// Using fmt package for error creation examples
fmt.Println("Using fmt package for error creation examples:")

// Basic error formatting with fmt.Errorf()
err1 := fmt.Errorf("operation failed: %s", "invalid input")
fmt.Printf("Error 1: %v\n", err1)
// Output: Error 1: operation failed: invalid input

// Error formatting with multiple values
err2 := fmt.Errorf("failed to process user %d: %s", 123, "not found")
fmt.Printf("Error 2: %v\n", err2)
// Output: Error 2: failed to process user 123: not found

// Error formatting with different types
err3 := fmt.Errorf("configuration error: host=%s, port=%d, ssl=%t", "localhost", 5432, true)
fmt.Printf("Error 3: %v\n", err3)
// Output: Error 3: configuration error: host=localhost, port=5432, ssl=true

// Error formatting in functions
func parseConfig(config map[string]string) error {
host, exists := config["host"]
if !exists {
return fmt.Errorf("missing required configuration: host")
}

portStr, exists := config["port"]
if !exists {
return fmt.Errorf("missing required configuration: port")
}

port, err := strconv.Atoi(portStr)
if err != nil {
return fmt.Errorf("invalid port configuration '%s': %v", portStr, err)
}

if port <= 0 || port > 65535 {
return fmt.Errorf("port %d is out of range (1-65535)", port)
}

return nil
}

// Test with valid configuration
validConfig := map[string]string{
"host": "localhost",
"port": "5432",
}

err := parseConfig(validConfig)
if err != nil {
fmt.Printf("Config error: %v\n", err)
} else {
fmt.Println("Configuration is valid")
}
// Output: Configuration is valid

// Test with missing host
invalidConfig1 := map[string]string{
"port": "5432",
}

err = parseConfig(invalidConfig1)
if err != nil {
fmt.Printf("Config error: %v\n", err)
} else {
fmt.Println("Configuration is valid")
}
// Output: Config error: missing required configuration: host

// Test with invalid port
invalidConfig2 := map[string]string{
"host": "localhost",
"port": "abc",
}

err = parseConfig(invalidConfig2)
if err != nil {
fmt.Printf("Config error: %v\n", err)
} else {
fmt.Println("Configuration is valid")
}
// Output: Config error: invalid port configuration 'abc': strconv.Atoi: parsing "abc": invalid syntax

// Error formatting with context
func processFile(filename string) error {
if filename == "" {
return fmt.Errorf("filename cannot be empty")
}

if len(filename) < 3 {
return fmt.Errorf("filename '%s' is too short (minimum 3 characters)", filename)
}

// Simulate file processing
if filename == "error.txt" {
return fmt.Errorf("failed to process file '%s': simulated error", filename)
}

return nil
}

// Test file processing
filenames := []string{"", "ab", "document.txt", "error.txt"}
for _, filename := range filenames {
err := processFile(filename)
if err != nil {
fmt.Printf("File processing error: %v\n", err)
} else {
fmt.Printf("File '%s' processed successfully\n", filename)
}
}
// Output:
// File processing error: filename cannot be empty
// File processing error: filename 'ab' is too short (minimum 3 characters)
// File 'document.txt' processed successfully
// File processing error: failed to process file 'error.txt': simulated error
}

Error Checking Patterns

Basic Error Checking

The if err != nil Pattern

The standard Go pattern for checking errors.

Error Handling in Functions

How to properly check and handle errors.

package main

import (
"errors"
"fmt"
"strconv"
)

func main() {
// Error checking patterns examples
fmt.Println("Error checking patterns examples:")

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

// Check error and handle it
result, err := safeDivide(10, 2)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %.2f\n", result)
}
// Output: Result: 5.00

// Check error and handle it
result, err = safeDivide(10, 0)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Result: %.2f\n", result)
}
// Output: Error: division by zero

// Error checking with early return
func processUser(userID string) error {
if userID == "" {
return errors.New("user ID cannot be empty")
}

// Simulate user processing
if userID == "invalid" {
return errors.New("invalid user ID")
}

fmt.Printf("Processing user: %s\n", userID)
return nil
}

// Handle error with early return
err := processUser("user123")
if err != nil {
fmt.Printf("Processing error: %v\n", err)
return
}
// Output: Processing user: user123

// Error checking in loops
func processUsers(userIDs []string) error {
for i, userID := range userIDs {
err := processUser(userID)
if err != nil {
return fmt.Errorf("failed to process user %d ('%s'): %w", i, userID, err)
}
}
return nil
}

// Test with valid users
validUsers := []string{"user1", "user2", "user3"}
err = processUsers(validUsers)
if err != nil {
fmt.Printf("Batch processing error: %v\n", err)
} else {
fmt.Println("All users processed successfully")
}
// Output:
// Processing user: user1
// Processing user: user2
// Processing user: user3
// All users processed successfully

// Test with invalid users
invalidUsers := []string{"user1", "invalid", "user3"}
err = processUsers(invalidUsers)
if err != nil {
fmt.Printf("Batch processing error: %v\n", err)
} else {
fmt.Println("All users processed successfully")
}
// Output:
// Processing user: user1
// Batch processing error: failed to process user 1 ('invalid'): invalid user ID

// Error checking with multiple operations
func performOperations(operations []string) error {
for i, op := range operations {
switch op {
case "validate":
// Simulate validation
continue
case "process":
// Simulate processing
continue
case "fail":
return fmt.Errorf("operation %d failed: %s", i, op)
default:
return fmt.Errorf("unknown operation: %s", op)
}
}
return nil
}

// Test with successful operations
ops1 := []string{"validate", "process", "validate"}
err = performOperations(ops1)
if err != nil {
fmt.Printf("Operations error: %v\n", err)
} else {
fmt.Println("All operations completed successfully")
}
// Output: All operations completed successfully

// Test with failing operation
ops2 := []string{"validate", "fail", "process"}
err = performOperations(ops2)
if err != nil {
fmt.Printf("Operations error: %v\n", err)
} else {
fmt.Println("All operations completed successfully")
}
// Output: Operations error: operation 1 failed: fail
}

Error Checking with Context

Adding Context to Errors

Providing additional context when errors occur.

Error Propagation with Context

How to propagate errors with meaningful context.

package main

import (
"errors"
"fmt"
"strconv"
)

func main() {
// Error checking with context examples
fmt.Println("Error checking with context examples:")

// Function that adds context to errors
func parseAndValidate(input string) (int, error) {
// Parse string to int
value, err := strconv.Atoi(input)
if err != nil {
return 0, fmt.Errorf("failed to parse '%s' as integer: %w", input, err)
}

// Validate range
if value < 0 {
return 0, fmt.Errorf("value %d is negative (must be non-negative)", value)
}

if value > 100 {
return 0, fmt.Errorf("value %d exceeds maximum (must be <= 100)", value)
}

return value, nil
}

// Test with valid input
value, err := parseAndValidate("42")
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed value: %d\n", value)
}
// Output: Parsed value: 42

// Test with invalid input
value, err = parseAndValidate("abc")
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed value: %d\n", value)
}
// Output: Error: failed to parse 'abc' as integer: strconv.Atoi: parsing "abc": invalid syntax

// Test with out of range input
value, err = parseAndValidate("150")
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("Parsed value: %d\n", value)
}
// Output: Error: value 150 exceeds maximum (must be <= 100)

// Function that processes data with context
func processData(data []string) ([]int, error) {
var results []int

for i, item := range data {
value, err := parseAndValidate(item)
if err != nil {
return nil, fmt.Errorf("failed to process item %d ('%s'): %w", i, item, err)
}
results = append(results, value)
}

return results, nil
}

// Test with valid data
validData := []string{"10", "20", "30", "40"}
results, err := processData(validData)
if err != nil {
fmt.Printf("Processing error: %v\n", err)
} else {
fmt.Printf("Results: %v\n", results)
}
// Output: Results: [10 20 30 40]

// Test with invalid data
invalidData := []string{"10", "abc", "30"}
results, err = processData(invalidData)
if err != nil {
fmt.Printf("Processing error: %v\n", err)
} else {
fmt.Printf("Results: %v\n", results)
}
// Output: Processing error: failed to process item 1 ('abc'): failed to parse 'abc' as integer: strconv.Atoi: parsing "abc": invalid syntax

// Function that handles errors with cleanup
func processWithCleanup(input string) error {
fmt.Printf("Starting processing of: %s\n", input)

// Simulate some work
value, err := parseAndValidate(input)
if err != nil {
fmt.Printf("Cleanup: processing failed for %s\n", input)
return fmt.Errorf("processing failed: %w", err)
}

// Simulate more work
if value%2 == 0 {
fmt.Printf("Processing successful: %d\n", value)
} else {
fmt.Printf("Cleanup: processing failed for odd number %d\n", value)
return fmt.Errorf("processing failed: odd numbers not allowed")
}

fmt.Printf("Cleanup: processing completed for %s\n", input)
return nil
}

// Test with valid input
err = processWithCleanup("42")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
// Output:
// Starting processing of: 42
// Processing successful: 42
// Cleanup: processing completed for 42

// Test with invalid input
err = processWithCleanup("abc")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
// Output:
// Starting processing of: abc
// Cleanup: processing failed for abc
// Error: processing failed: failed to parse 'abc' as integer: strconv.Atoi: parsing "abc": invalid syntax

// Test with odd number
err = processWithCleanup("43")
if err != nil {
fmt.Printf("Error: %v\n", err)
}
// Output:
// Starting processing of: 43
// Cleanup: processing failed for odd number 43
// Error: processing failed: odd numbers not allowed
}

Error Propagation

Understanding Error Propagation

Error Propagation Patterns

How errors flow through the call stack.

Context Preservation

Maintaining error context as errors propagate.

package main

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

func main() {
// Error propagation examples
fmt.Println("Error propagation examples:")

// Function that reads from a file
func readFile(filename string) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("failed to open file '%s': %w", filename, err)
}
defer file.Close()

data, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("failed to read file '%s': %w", filename, err)
}

return data, nil
}

// Function that processes file content
func processFile(filename string) (int, error) {
data, err := readFile(filename)
if err != nil {
return 0, fmt.Errorf("failed to process file '%s': %w", filename, err)
}

// Simulate processing
if len(data) == 0 {
return 0, fmt.Errorf("file '%s' is empty", filename)
}

return len(data), nil
}

// Function that handles multiple files
func processFiles(filenames []string) (map[string]int, error) {
results := make(map[string]int)

for _, filename := range filenames {
size, err := processFile(filename)
if err != nil {
return nil, fmt.Errorf("failed to process files: %w", err)
}
results[filename] = size
}

return results, nil
}

// Test with non-existent file
size, err := processFile("nonexistent.txt")
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Printf("File size: %d bytes\n", size)
}
// Output: Error: failed to process file 'nonexistent.txt': failed to open file 'nonexistent.txt': open nonexistent.txt: no such file or directory

// Error propagation with validation
func validateUser(userID string) error {
if userID == "" {
return errors.New("user ID cannot be empty")
}

if len(userID) < 3 {
return fmt.Errorf("user ID '%s' is too short (minimum 3 characters)", userID)
}

return nil
}

func getUser(userID string) (map[string]string, error) {
if err := validateUser(userID); err != nil {
return nil, fmt.Errorf("failed to get user: %w", err)
}

// Simulate user lookup
if userID == "admin" {
return map[string]string{
"id": userID,
"name": "Administrator",
"email": "[email protected]",
}, nil
}

return nil, fmt.Errorf("user '%s' not found", userID)
}

func updateUser(userID string, updates map[string]string) error {
user, err := getUser(userID)
if err != nil {
return fmt.Errorf("failed to update user: %w", err)
}

// Simulate update
for key, value := range updates {
user[key] = value
}

return nil
}

// Test with valid user
updates := map[string]string{"name": "Updated Name"}
err = updateUser("admin", updates)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("User updated successfully")
}
// Output: User updated successfully

// Test with invalid user ID
err = updateUser("", updates)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("User updated successfully")
}
// Output: Error: failed to update user: failed to get user: user ID cannot be empty

// Test with non-existent user
err = updateUser("nonexistent", updates)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("User updated successfully")
}
// Output: Error: failed to update user: failed to get user: user 'nonexistent' not found

// Error propagation with multiple operations
func performOperations(operations []string) error {
for i, op := range operations {
switch op {
case "validate":
if err := validateUser("test"); err != nil {
return fmt.Errorf("operation %d ('%s') failed: %w", i, op, err)
}
case "process":
// Simulate processing
continue
case "fail":
return fmt.Errorf("operation %d ('%s') failed: simulated failure", i, op)
default:
return fmt.Errorf("operation %d ('%s') failed: unknown operation", i, op)
}
}
return nil
}

// Test with successful operations
ops1 := []string{"validate", "process", "validate"}
err = performOperations(ops1)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All operations completed successfully")
}
// Output: All operations completed successfully

// Test with failing operation
ops2 := []string{"validate", "fail", "process"}
err = performOperations(ops2)
if err != nil {
fmt.Printf("Error: %v\n", err)
} else {
fmt.Println("All operations completed successfully")
}
// Output: Error: operation 1 ('fail') failed: simulated failure
}

What You've Learned

Congratulations! You now have a comprehensive understanding of Go's error interface and basic error handling:

Error Interface

  • Understanding the built-in error interface and its simplicity
  • Working with error values and error methods
  • Creating errors using the errors and fmt packages
  • Understanding implicit error interface implementation

Error Creation

  • Using errors.New() for simple error creation
  • Using fmt.Errorf() for formatted error creation
  • Creating errors with context and dynamic content
  • Building error messages with meaningful information

Error Checking Patterns

  • Implementing the standard if err != nil pattern
  • Handling errors in functions and loops
  • Using early returns for error handling
  • Implementing error checking with cleanup operations

Error Propagation

  • Understanding how errors flow through the call stack
  • Adding context to errors as they propagate
  • Preserving error information and context
  • Building robust error handling systems

Key Concepts

  • error - The built-in error interface in Go
  • errors.New() - Function for creating simple errors
  • fmt.Errorf() - Function for creating formatted errors
  • Error checking - The if err != nil pattern
  • Error propagation - How errors flow through the call stack

Next Steps

You now have a solid foundation in Go's error interface and basic error handling. In the next section, we'll explore custom error types and panic/recover mechanisms, which build upon these basic concepts.

Understanding the error interface and basic error handling is crucial for writing robust Go applications. These concepts form the foundation for all the more advanced error handling techniques we'll cover in the coming chapters.


Ready to learn about custom error types and panic/recover? Let's explore advanced error handling techniques and exceptional circumstances!