Error Handling Patterns
Go error values vs Python exceptions — wrapping, unwrapping, and sentinel errors
Introduction
In this lesson, you'll learn about error handling patterns in Go. Coming from Python, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In Python, you're familiar with go error values vs python exceptions — wrapping, unwrapping, and sentinel errors.
Go has its own approach to go error values vs python exceptions — wrapping, unwrapping, and sentinel errors, which we'll explore step by step.
The Go Way
Let's see how Go handles this concept. Here's a typical example:
package main
import (
"errors"
"fmt"
)
// Custom error type
type NotFoundError struct {
Resource string
ID int
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s %d not found", e.Resource, e.ID)
}
// Sentinel errors (like Python's exception classes used as signals)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
// Function that returns error
func GetUser(id int) (map[string]any, error) {
if id < 0 {
return nil, fmt.Errorf("id must be positive, got %d", id)
}
if id > 100 {
return nil, &NotFoundError{Resource: "User", ID: id}
}
return map[string]any{"id": id}, nil
}
// Error wrapping with %w
func LoadConfig(path string) (Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return Config{}, fmt.Errorf("loadConfig %s: %w", path, err)
}
return parseConfig(data)
}
func main() {
user, err := GetUser(999)
if err != nil {
var nfe *NotFoundError
if errors.As(err, &nfe) {
fmt.Printf("Not found: %s #%d\n", nfe.Resource, nfe.ID)
} else {
fmt.Println("Error:", err)
}
return
}
_ = user
}Comparing to Python
Here's how you might have written similar code in Python:
# Custom exception hierarchy
class AppError(Exception):
pass
class NotFoundError(AppError):
def __init__(self, resource: str, id: int):
super().__init__(f"{resource} {id} not found")
self.resource = resource
self.id = id
# Raise and catch
def get_user(id: int):
if id < 0:
raise ValueError("id must be positive")
if id > 100:
raise NotFoundError("User", id)
return {"id": id}
try:
user = get_user(999)
except NotFoundError as e:
print(f"Not found: {e.resource} #{e.id}")
except ValueError as e:
print(f"Bad input: {e}")
# Chaining exceptions
try:
parse_config()
except json.JSONDecodeError as e:
raise AppError("config invalid") from eYou may be used to different syntax or behavior.
Go returns errors as values; Python throws exceptions — completely different control flow
You may be used to different syntax or behavior.
errors.As extracts concrete error type — equivalent to except NotFoundError as e
You may be used to different syntax or behavior.
errors.Is checks sentinel errors through the chain — like checking exception type
You may be used to different syntax or behavior.
fmt.Errorf with %w wraps errors (like Python 'raise X from Y')
You may be used to different syntax or behavior.
Go has no try/catch — caller must check err != nil after every call
Step-by-Step Breakdown
1. Errors as Values
Go errors are values returned alongside results. Every fallible function returns (result, error). Check error before using result.
try:
user = get_user(id)
except NotFoundError:
handle()user, err := GetUser(id)
if err != nil {
handle(err)
return
}
// safe to use user here2. Custom Error Types
Implement the error interface (Error() string) for custom error types. Use pointer receivers so errors.As can match.
class NotFoundError(Exception):
def __init__(self, id): self.id = idtype NotFoundError struct { ID int }
func (e *NotFoundError) Error() string {
return fmt.Sprintf("not found: %d", e.ID)
}3. Error Wrapping
Wrap errors with context using fmt.Errorf("%w", err). This preserves the original error so errors.Is/As can unwrap the chain.
try:
parse()
except json.JSONDecodeError as e:
raise AppError("parse failed") from eif err := parse(); err != nil {
return fmt.Errorf("parse failed: %w", err)
// %w wraps; caller can errors.Is(err, originalErr)
}4. errors.As and errors.Is
errors.As extracts a concrete type from an error chain. errors.Is checks for sentinel errors. Both unwrap through the chain.
except NotFoundError as e: ...
except PermissionError: ...var nfe *NotFoundError
if errors.As(err, &nfe) { /* nfe is populated */ }
if errors.Is(err, ErrNotFound) { /* matches sentinel */ }Common Mistakes
When coming from Python, developers often make these mistakes:
- Go returns errors as values; Python throws exceptions — completely different control flow
- errors.As extracts concrete error type — equivalent to except NotFoundError as e
- errors.Is checks sentinel errors through the chain — like checking exception type
Key Takeaways
- Go errors are return values — check every err != nil; no try/catch
- Custom errors: implement Error() string interface; use pointer receiver
- fmt.Errorf("%w", err) wraps errors with context — like 'raise X from Y'
- errors.As extracts concrete type from chain; errors.Is checks sentinel values