Go Error Handling
Managing errors gracefully in Go programs
⚠️ What is Error Handling?
Go uses explicit error handling through return values rather than exceptions. Functions return an error as the last value, making error handling visible and forcing developers to handle potential failures explicitly.
// Function that can return an error
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
// Always check for errors
result, err := divide(10, 0)
if err != nil {
fmt.Println("Error:", err)
}
Output:
Error: division by zero
Key Error Concepts
Error Interface
Built-in error type with Error() method
type error interface {
Error() string
}
Explicit Checking
Always check if err != nil
if err != nil {
// Handle error
}
Error Creation
Create errors with errors.New()
err := errors.New("something went wrong")
Error Wrapping
Add context to errors
fmt.Errorf("failed to process: %w", err)
🔹 Basic Error Handling
The fundamental pattern of error handling in Go:
package main
import (
"errors"
"fmt"
"strconv"
)
func parseAge(s string) (int, error) {
age, err := strconv.Atoi(s)
if err != nil {
return 0, fmt.Errorf("invalid age format: %s", s)
}
if age < 0 {
return 0, errors.New("age cannot be negative")
}
if age > 150 {
return 0, errors.New("age seems unrealistic")
}
return age, nil
}
func main() {
testCases := []string{"25", "abc", "-5", "200"}
for _, test := range testCases {
age, err := parseAge(test)
if err != nil {
fmt.Printf("Error parsing '%s': %v\n", test, err)
} else {
fmt.Printf("Valid age: %d\n", age)
}
}
}
Output:
Valid age: 25
Error parsing 'abc': invalid age format: abc
Error parsing '-5': age cannot be negative
Error parsing '200': age seems unrealistic
🔹 Custom Error Types
Create custom error types for more detailed error information:
// Custom error type
type ValidationError struct {
Field string
Value interface{}
Message string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("validation failed for field '%s' with value '%v': %s",
e.Field, e.Value, e.Message)
}
type User struct {
Name string
Email string
Age int
}
func validateUser(u User) error {
if u.Name == "" {
return ValidationError{
Field: "Name",
Value: u.Name,
Message: "name cannot be empty",
}
}
if u.Age < 0 || u.Age > 120 {
return ValidationError{
Field: "Age",
Value: u.Age,
Message: "age must be between 0 and 120",
}
}
if !strings.Contains(u.Email, "@") {
return ValidationError{
Field: "Email",
Value: u.Email,
Message: "email must contain @ symbol",
}
}
return nil
}
func main() {
users := []User{
{Name: "John", Email: "[email protected]", Age: 30},
{Name: "", Email: "[email protected]", Age: 25},
{Name: "Jane", Email: "invalid-email", Age: 28},
{Name: "Bob", Email: "[email protected]", Age: -5},
}
for i, user := range users {
if err := validateUser(user); err != nil {
fmt.Printf("User %d validation failed: %v\n", i+1, err)
} else {
fmt.Printf("User %d is valid: %s\n", i+1, user.Name)
}
}
}
Output:
User 1 is valid: John
User 2 validation failed: validation failed for field 'Name' with value '': name cannot be empty
User 3 validation failed: validation failed for field 'Email' with value 'invalid-email': email must contain @ symbol
User 4 validation failed: validation failed for field 'Age' with value '-5': age must be between 0 and 120
🔹 Error Wrapping and Unwrapping
Add context to errors while preserving the original error:
import (
"errors"
"fmt"
)
func readConfig(filename string) error {
return errors.New("file not found")
}
func loadDatabase() error {
err := readConfig("database.conf")
if err != nil {
return fmt.Errorf("failed to load database config: %w", err)
}
return nil
}
func startApplication() error {
err := loadDatabase()
if err != nil {
return fmt.Errorf("application startup failed: %w", err)
}
return nil
}
func main() {
err := startApplication()
if err != nil {
fmt.Printf("Error: %v\n", err)
// Unwrap to get the original error
var originalErr error = err
for originalErr != nil {
fmt.Printf(" -> %v\n", originalErr)
originalErr = errors.Unwrap(originalErr)
}
// Check if the error chain contains a specific error
if errors.Is(err, errors.New("file not found")) {
fmt.Println("Root cause: Configuration file is missing")
}
}
}
Output:
Error: application startup failed: failed to load database config: file not found
-> application startup failed: failed to load database config: file not found
-> failed to load database config: file not found
-> file not found
🔹 Error Handling Patterns
Common patterns for handling errors effectively:
// Pattern 1: Early return
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("cannot open file: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("cannot read file: %w", err)
}
if err := processData(data); err != nil {
return fmt.Errorf("cannot process data: %w", err)
}
return nil
}
// Pattern 2: Error aggregation
func validateMultiple(items []string) []error {
var errors []error
for i, item := range items {
if item == "" {
errors = append(errors, fmt.Errorf("item %d is empty", i))
}
if len(item) > 100 {
errors = append(errors, fmt.Errorf("item %d is too long", i))
}
}
return errors
}
// Pattern 3: Sentinel errors
var (
ErrNotFound = errors.New("item not found")
ErrInvalidInput = errors.New("invalid input")
ErrTimeout = errors.New("operation timed out")
)
func findItem(id int) (string, error) {
if id < 0 {
return "", ErrInvalidInput
}
if id > 1000 {
return "", ErrNotFound
}
return fmt.Sprintf("item-%d", id), nil
}
func main() {
// Test sentinel errors
item, err := findItem(-1)
if err != nil {
if errors.Is(err, ErrInvalidInput) {
fmt.Println("Please provide a valid ID")
} else if errors.Is(err, ErrNotFound) {
fmt.Println("Item does not exist")
} else {
fmt.Printf("Unexpected error: %v\n", err)
}
} else {
fmt.Printf("Found: %s\n", item)
}
}
Output:
Please provide a valid ID