🚨 Error Handling in Go: Your Safety Net for Code
Imagine you’re a detective 🕵️ solving mysteries. Every time something goes wrong in your program, errors are the clues that help you figure out what happened!
The Big Picture: What Are Errors?
Think of errors like warning signs on a road. When you’re driving, signs tell you “Bridge Out Ahead!” or “Slippery When Wet!” In Go, errors tell your program “Hey, something didn’t work as expected!”
Why is this important?
- Programs talk to the internet (it might be down!)
- Programs read files (the file might not exist!)
- Programs do math (you can’t divide by zero!)
Go helps you handle these problems gracefully instead of crashing like a car without brakes.
🎯 The error Interface
What is it?
In Go, an error is super simple. It’s just anything that can say what went wrong.
type error interface {
Error() string
}
That’s it! If something has an Error() method that returns a string, it’s an error.
Real-Life Analogy
Imagine every problem in life had to fill out a form with ONE question:
“What went wrong?”
That’s what the error interface does. Any type that can answer this question is an error!
Simple Example
package main
import "fmt"
func divide(a, b int) (int, error) {
if b == 0 {
return 0, fmt.Errorf("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("Oops:", err)
return
}
fmt.Println("Result:", result)
}
Output: Oops: cannot divide by zero
✨ Error Creation
Three Ways to Create Errors
Go gives you multiple tools to create errors. Like having different pens for different jobs!
1️⃣ errors.New() - The Simple Way
import "errors"
err := errors.New("something went wrong")
Use this when you need a basic error message.
2️⃣ fmt.Errorf() - The Smart Way
import "fmt"
name := "config.txt"
err := fmt.Errorf("file not found: %s", name)
Use this when you want to include details in your error.
When to Use Each?
| Method | Best For |
|---|---|
errors.New() |
Simple, fixed messages |
fmt.Errorf() |
Messages with variables |
🎁 Error Wrapping
The Problem
Imagine you’re passing a message in a game of telephone. Each person adds context:
- Person 1: “The cake fell”
- Person 2: “While carrying the cake, the cake fell”
- Person 3: “At the party, while carrying the cake, the cake fell”
That’s error wrapping! You keep the original error but add more context.
How to Wrap Errors
Use fmt.Errorf() with the special %w verb:
originalErr := errors.New("disk full")
wrappedErr := fmt.Errorf("save failed: %w", originalErr)
fmt.Println(wrappedErr)
// Output: save failed: disk full
Why Wrap?
graph TD A["Original: disk full"] --> B["save failed: disk full"] B --> C["backup failed: save failed: disk full"] C --> D["User sees full story!"]
Without wrapping, you’d only know “backup failed” but not WHY!
🔗 errors.Join - Combining Multiple Errors
The Scenario
What if TWO things go wrong at the same time? Like spilling your drink AND dropping your phone?
The Solution
import "errors"
err1 := errors.New("database connection failed")
err2 := errors.New("cache connection failed")
combined := errors.Join(err1, err2)
fmt.Println(combined)
Output:
database connection failed
cache connection failed
Visual
graph TD A["Error 1: DB failed"] --> C["Combined Error"] B["Error 2: Cache failed"] --> C C --> D["Both problems reported!"]
🔍 Error Inspection
Two Superpowers: errors.Is() and errors.As()
errors.Is() - “Is this THE error?”
Like asking: “Is this the SAME person?” even if they’re wearing a disguise (wrapped).
var ErrNotFound = errors.New("not found")
err := fmt.Errorf("user lookup: %w", ErrNotFound)
if errors.Is(err, ErrNotFound) {
fmt.Println("The item doesn't exist!")
}
Even though err is wrapped, errors.Is() can see through the wrapping!
errors.As() - “Can I use this error’s special features?”
Like asking: “Is this person a doctor? If so, let me talk to them AS a doctor.”
var pathErr *os.PathError
if errors.As(err, &pathErr) {
fmt.Println("Problem with path:", pathErr.Path)
}
Quick Reference
| Function | Question It Answers |
|---|---|
errors.Is(err, target) |
Is this error equal to target? |
errors.As(err, &target) |
Can I treat this error as type target? |
🚦 Sentinel Errors
What Are They?
Sentinel errors are like traffic lights - predefined signals everyone recognizes.
var ErrNotFound = errors.New("item not found")
var ErrPermission = errors.New("permission denied")
var ErrTimeout = errors.New("operation timed out")
Why Use Them?
Instead of checking error messages (which can change), you check the EXACT error:
// ❌ Bad - message might change
if err.Error() == "item not found" { ... }
// ✅ Good - checking the exact error
if errors.Is(err, ErrNotFound) { ... }
Naming Convention
Start sentinel errors with Err:
ErrNotFoundErrInvalidInputErrTimeout
Real Example
package storage
var ErrNotFound = errors.New("item not found")
func GetUser(id int) (*User, error) {
user := findInDB(id)
if user == nil {
return nil, ErrNotFound
}
return user, nil
}
// In another package
user, err := storage.GetUser(123)
if errors.Is(err, storage.ErrNotFound) {
fmt.Println("User doesn't exist!")
}
🎨 Custom Error Types
When Simple Errors Aren’t Enough
Sometimes you need errors that carry extra information. Like a doctor’s note that includes your temperature, not just “you’re sick.”
Creating a Custom Error Type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
Using Custom Errors
func ValidateAge(age int) error {
if age < 0 {
return &ValidationError{
Field: "age",
Message: "cannot be negative",
}
}
return nil
}
func main() {
err := ValidateAge(-5)
var valErr *ValidationError
if errors.As(err, &valErr) {
fmt.Printf("Field '%s' has error: %s\n",
valErr.Field, valErr.Message)
}
}
Output: Field 'age' has error: cannot be negative
Custom Error with Extra Methods
type HTTPError struct {
StatusCode int
Message string
}
func (e *HTTPError) Error() string {
return fmt.Sprintf("HTTP %d: %s",
e.StatusCode, e.Message)
}
func (e *HTTPError) IsClientError() bool {
return e.StatusCode >= 400 &&
e.StatusCode < 500
}
🗺️ The Complete Picture
graph TD A["Error Handling in Go"] --> B["error Interface"] A --> C["Creating Errors"] A --> D["Error Wrapping"] A --> E["errors.Join"] A --> F["Error Inspection"] A --> G["Sentinel Errors"] A --> H["Custom Error Types"] C --> C1["errors.New"] C --> C2["fmt.Errorf"] F --> F1["errors.Is"] F --> F2["errors.As"]
💡 Golden Rules
- Always check errors - Never ignore them!
- Wrap errors - Add context as errors bubble up
- Use sentinel errors - For errors you want to compare
- Use custom types - When you need extra data
- Use
errors.Is()anderrors.As()- Not string comparison
🎉 You Did It!
You now understand Go’s error handling system! Remember:
Errors are your friends, not your enemies. They tell you exactly what went wrong so you can fix it!
Like a good detective, you now have all the tools to investigate any problem in your Go programs. Happy coding! 🚀
