Go Error Handling - Complete Guide
Error handling in Go is different from other languages. Go doesn't have exceptions - instead, functions return errors as values. This makes error handling explicit and predictable!
Go's Error Philosophy
In Go, errors are values. Functions return errors, and you check them explicitly. No hidden exceptions, no surprises!
result, err := doSomething()
if err != nil {
// Handle error
return err
}
// Use resultThe Error Interface
Go has a built-in error interface:
type error interface {
Error() string
}Any type with an Error() string method implements the error interface!
Creating Errors
errors.New()
import "errors"
err := errors.New("something went wrong")fmt.Errorf()
import "fmt"
err := fmt.Errorf("user %s not found", username)Custom Error Types
type NotFoundError struct {
Resource string
ID int
}
func (e NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %d not found", e.Resource, e.ID)
}
func findUser(id int) (*User, error) {
// If not found:
return nil, NotFoundError{Resource: "user", ID: id}
}Handling Errors
Basic Error Handling
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division 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)
}Error Wrapping
Wrap errors to add context:
import "fmt"
func processUser(id int) error {
user, err := getUser(id)
if err != nil {
return fmt.Errorf("failed to process user: %w", err)
}
// Process user...
return nil
}Checking Error Types
var notFound NotFoundError
if errors.As(err, ¬Found) {
fmt.Println("Not found:", notFound.Resource)
}Unwrapping Errors
import "errors"
wrappedErr := fmt.Errorf("outer: %w", innerErr)
unwrapped := errors.Unwrap(wrappedErr)Common Error Patterns
Pattern 1: Early Return
func process(data string) error {
if data == "" {
return errors.New("data is empty")
}
if len(data) < 5 {
return errors.New("data too short")
}
// Process data
return nil
}Pattern 2: Error Propagation
func getUser(id int) (*User, error) {
data, err := fetchData(id)
if err != nil {
return nil, err // Propagate error
}
user, err := parseUser(data)
if err != nil {
return nil, err // Propagate error
}
return user, nil
}Pattern 3: Error Wrapping
func processFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", filename, err)
}
defer file.Close()
// Process file...
return nil
}Sentinel Errors
Predefined errors you can compare:
var ErrNotFound = errors.New("not found")
var ErrInvalidInput = errors.New("invalid input")
func find(id int) (*Item, error) {
if id < 0 {
return nil, ErrInvalidInput
}
// If not found:
return nil, ErrNotFound
}
// Usage
item, err := find(5)
if err == ErrNotFound {
fmt.Println("Item not found")
}Error Handling in Goroutines
Handle errors in concurrent code:
func worker(id int, results chan<- Result, errors chan<- error) {
result, err := doWork(id)
if err != nil {
errors <- err
return
}
results <- result
}
func main() {
results := make(chan Result)
errors := make(chan error)
for i := 0; i < 10; i++ {
go worker(i, results, errors)
}
// Collect results and errors
for i := 0; i < 10; i++ {
select {
case result := <-results:
fmt.Println("Result:", result)
case err := <-errors:
fmt.Println("Error:", err)
}
}
}Visual Explanation: Error Flow
Here's how errors propagate:
Function Call:
┌─────────────────────────────┐
│ func doSomething() error { │
│ if error { │
│ return error │
│ } │
│ return nil │
│ } │
└──────────────┬──────────────┘
│
▼
┌──────────────┐
│ Caller │
│ │
│ err := │
│ doSomething()│
│ │
│ if err != nil│
│ handle │
└──────────────┘Best Practices
1. Always Check Errors
// Bad
result, _ := doSomething() // Ignoring error!
// Good
result, err := doSomething()
if err != nil {
return err
}2. Add Context
// Bad
return err
// Good
return fmt.Errorf("failed to process user %d: %w", userID, err)3. Use Sentinel Errors
var ErrNotFound = errors.New("not found")
// Easy to check
if err == ErrNotFound {
// Handle not found
}4. Don't Panic (Usually)
// Bad - Don't panic for normal errors
if err != nil {
panic(err)
}
// Good - Return error
if err != nil {
return err
}Frequently Asked Questions (FAQ)
Q1: Why doesn't Go have exceptions?
A: Go's designers wanted explicit error handling. Returning errors makes it clear what can fail. No hidden exceptions that might crash your program!
Q2: Should I use panic?
A: Only for truly unrecoverable errors (like programming bugs). For normal errors, return error values.
Q3: How do I create custom errors?
A: Create a type with Error() string method:
type MyError struct {
Message string
}
func (e MyError) Error() string {
return e.Message
}Q4: What's the difference between errors.New and fmt.Errorf?
A:
- errors.New - Simple error message
- fmt.Errorf - Formatted error message, can wrap errors with
%w
Q5: How do I check error types?
A: Use errors.As:
var myErr MyError
if errors.As(err, &myErr) {
// Handle MyError
}Q6: Can I ignore errors?
A: Technically yes with _, but it's usually a bad idea:
result, _ := doSomething() // Bad!Always handle errors!
Q7: How do I wrap errors?
A: Use fmt.Errorf with %w:
return fmt.Errorf("context: %w", err)Q8: What's a sentinel error?
A: A predefined error you can compare:
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// Handle
}Q9: How do I handle errors in loops?
A: Check and handle in each iteration:
for _, item := range items {
if err := process(item); err != nil {
log.Printf("Error processing %v: %v", item, err)
continue // Skip this item
}
}Q10: Should errors be part of function signature?
A: Yes! If a function can fail, it should return an error:
func doSomething() (Result, error) {
// ...
}Next Steps
Now that you understand error handling, here's what to learn next:
- Next: Learn about Go Testing - Test error cases
- Explore Go REST API - Handle API errors
- Understand Go Concurrency - Handle errors in goroutines
- Master Go Best Practices - Write robust code
Error handling is crucial in Go. Always check errors and handle them properly!