Error Handling
Lesson 10: Error Handling
Real-World Analogy
Imagine you go to a restaurant to order food. The waiter tells you: "Sorry, this dish is sold out today." — This is an expected situation; you can order something else, and life goes on. This is error in Go.
But if the kitchen suddenly catches fire and everyone must evacuate immediately — This is an unexpected disaster. This is panic in Go.
Go's error handling philosophy is straightforward: handle anticipated problems gracefully, and only crash for unanticipated ones. Unlike other languages that mix all exceptions with try/catch, Go requires you to handle each error explicitly and individually.
Core Concepts
| Concept | Description |
|---|---|
error interface |
Go's built-in error type with only one method: Error() string |
| Multiple return values | Go functions typically return errors as the last return value |
errors.New |
Creates a simple text error |
fmt.Errorf |
Creates a formatted error (can wrap with %w) |
errors.Is |
Checks if an error chain contains a specific error |
errors.As |
Extracts a specific error type from an error chain |
panic |
Triggers a runtime panic, crashing the program |
recover |
Captures panic in defer to prevent program crash |
| Error wrapping | Wrap underlying errors with %w or custom types to preserve context |
Error Handling Flowchart
Function returns error
│
▼
err != nil ?
┌────┴────┐
│ Yes │ No
▼ ▼
Handle error Continue
│
├─ Recoverable → Log / Return default / Retry
├─ Needs reporting → Wrap and pass up
└─ Unrecoverable → panic
Basic Syntax and Usage
1. error Interface
error is Go's built-in interface type, defined very concisely:
// error interface definition (built-in, no import needed)
type error interface {
Error() string
}
Any type that implements the Error() string method is an error.
2. Creating Errors
package main
import (
"errors"
"fmt"
)
func main() {
// Method 1: errors.New — create a simple text error
err1 := errors.New("file not found")
// Method 2: fmt.Errorf — create a formatted error
filename := "config.yaml"
err2 := fmt.Errorf("failed to read file %s", filename)
// Method 3: fmt.Errorf + %w — wrap underlying error (recommended)
baseErr := errors.New("permission denied")
err3 := fmt.Errorf("cannot write log: %w", baseErr)
fmt.Println(err1) // file not found
fmt.Println(err2) // failed to read file config.yaml
fmt.Println(err3) // cannot write log: permission denied
}
3. Checking Errors
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("record not found")
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return "Alice", nil
}
func main() {
name, err := findUser(0)
if err != nil {
// errors.Is checks if the error chain contains the target error
if errors.Is(err, ErrNotFound) {
fmt.Println("User not found, please check the ID")
} else {
fmt.Println("Unknown error:", err)
}
return
}
fmt.Println("Found user:", name)
}
4. Extracting Specific Error Types
package main
import (
"errors"
"fmt"
)
// Custom error type
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed [%s]: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 || age > 150 {
return &ValidationError{
Field: "age",
Message: "age must be between 0 and 150",
}
}
return nil
}
func main() {
err := validateAge(200)
if err != nil {
// errors.As extracts a specific type from the error chain
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("Field: %s, Reason: %s\n", ve.Field, ve.Message)
} else {
fmt.Println("Other error:", err)
}
}
}
5. panic and recover
package main
import "fmt"
// safeDivide uses recover to catch panic
func safeDivide(a, b int) (result int, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("caught panic: %v", r)
}
}()
// Dividing by zero triggers a panic
return a / b, nil
}
func main() {
result, err := safeDivide(10, 0)
if err != nil {
fmt.Println("Error:", err) // Error: caught panic: runtime error: integer divide by zero
return
}
fmt.Println("Result:", result)
}
error, not panic. Only use panic when the program truly cannot continue (e.g., initialization failure, unrecoverable logic errors).
fmt.Errorf, always use %w instead of %v. %w preserves the original error, allowing errors.Is and errors.As to work correctly.
Examples
Example: Basic Error Handling (Difficulty ⭐)
Simulate a simple configuration file reader to demonstrate the most basic error return and checking pattern.
package main
import (
"errors"
"fmt"
"os"
)
// Define package-level sentinel errors
var (
ErrFileNotFound = errors.New("config file not found")
ErrFileEmpty = errors.New("config file is empty")
ErrInvalidFormat = errors.New("invalid config file format")
)
// Config represents application configuration
type Config struct {
Host string
Port int
}
// LoadConfig loads configuration from a file (simulated)
func LoadConfig(path string) (*Config, error) {
// Simulate file not found
if path == "" {
return nil, ErrFileNotFound
}
// Simulate reading file
data, err := os.ReadFile(path)
if err != nil {
// Wrap underlying error, preserving original info
return nil, fmt.Errorf("reading config file %s: %w", path, err)
}
// Check if file is empty
if len(data) == 0 {
return nil, ErrFileEmpty
}
// Simulate parsing config
return &Config{
Host: "localhost",
Port: 8080,
}, nil
}
func main() {
// Try to load config
config, err := LoadConfig("app.conf")
if err != nil {
// Use errors.Is to determine specific error type
switch {
case errors.Is(err, ErrFileNotFound):
fmt.Println("Error: Please specify the config file path")
case errors.Is(err, ErrFileEmpty):
fmt.Println("Error: Config file is empty, please check contents")
case errors.Is(err, ErrInvalidFormat):
fmt.Println("Error: Config file format is invalid")
default:
fmt.Println("Error:", err)
}
return
}
fmt.Printf("Config loaded successfully: %s:%d\n", config.Host, config.Port)
}
Output (when file doesn't exist):
Error: Please specify the config file path
Example: Error Wrapping and Chain Checking (Difficulty ⭐⭐)
Demonstrate error wrapping, propagation, and chain checking in multi-layer function calls.
package main
import (
"errors"
"fmt"
)
// Define business errors
var (
ErrUserNotFound = errors.New("user not found")
ErrPermission = errors.New("insufficient permissions")
)
// Represents database layer errors
type DatabaseError struct {
Operation string
Table string
Err error
}
func (e *DatabaseError) Error() string {
return fmt.Sprintf("database error [%s.%s]: %e", e.Table, e.Operation, e.Err)
}
func (e *DatabaseError) Unwrap() error {
return e.Err
}
// UserRepository user repository
type UserRepository struct {
users map[int]string
}
// FindByID finds a user by ID
func (r *UserRepository) FindByID(id int) (string, error) {
name, exists := r.users[id]
if !exists {
// Wrap as database layer error
return "", &DatabaseError{
Operation: "SELECT",
Table: "users",
Err: ErrUserNotFound,
}
}
return name, nil
}
// Service business service layer
type Service struct {
repo *UserRepository
}
// GetUser gets user info
func (s *Service) GetUser(id int, requireAdmin bool) (string, error) {
name, err := s.repo.FindByID(id)
if err != nil {
// Wrap upward, adding business context
return "", fmt.Errorf("getting user info (id=%d): %w", id, err)
}
if requireAdmin && name != "admin" {
return "", fmt.Errorf("user %s: %w", name, ErrPermission)
}
return name, nil
}
func main() {
repo := &UserRepository{
users: map[int]string{
1: "admin",
2: "Alice",
},
}
svc := &Service{repo: repo}
// Test scenarios
testCases := []struct {
name string
id int
requireAdmin bool
}{
{"Find admin", 1, false},
{"Find regular user", 2, false},
{"Find non-existent user", 99, false},
{"Regular user accessing admin function", 2, true},
}
for _, tc := range testCases {
fmt.Printf("--- %s ---\n", tc.name)
user, err := svc.GetUser(tc.id, tc.requireAdmin)
if err != nil {
fmt.Printf(" Failed: %v\n", err)
// errors.Is can traverse the error chain to find root cause
if errors.Is(err, ErrUserNotFound) {
fmt.Println(" Action: Prompt user to check ID")
} else if errors.Is(err, ErrPermission) {
fmt.Println(" Action: Prompt user to contact admin")
}
// errors.As can extract specific error types from intermediate layers
var dbErr *DatabaseError
if errors.As(err, &dbErr) {
fmt.Printf(" DB details: table=%s, operation=%s\n", dbErr.Table, dbErr.Operation)
}
} else {
fmt.Printf(" Success: User %s\n", user)
}
fmt.Println()
}
}
Output:
--- Find admin ---
Success: User admin
--- Find regular user ---
Success: User Alice
--- Find non-existent user ---
Failed: getting user info (id=99): database error [users.SELECT]: user not found
Action: Prompt user to check ID
DB details: table=users, operation=SELECT
--- Regular user accessing admin function ---
Failed: User Alice: insufficient permissions
Action: Prompt user to contact admin
Example: Custom Error Types and recover in Practice (Difficulty ⭐⭐⭐)
Implement a complete HTTP request handler with custom error types, error codes, and a panic recovery middleware.
package main
import (
"errors"
"fmt"
"runtime/debug"
)
// ==================== Custom Error System ====================
// AppError application-level error with error code and context
type AppError struct {
Code int // Business error code
Message string // User-friendly message
Detail string // Developer debug info
Cause error // Underlying cause
Context map[string]string // Additional context
}
func (e *AppError) Error() string {
if e.Cause != nil {
return fmt.Sprintf("[%d] %s: %v", e.Code, e.Message, e.Cause)
}
return fmt.Sprintf("[%d] %s", e.Code, e.Message)
}
func (e *AppError) Unwrap() error {
return e.Cause
}
// Is implements custom error comparison logic
func (e *AppError) Is(target error) bool {
t, ok := target.(*AppError)
if !ok {
return false
}
return e.Code == t.Code
}
// NewAppError creates a new application error
func NewAppError(code int, message string) *AppError {
return &AppError{
Code: code,
Message: message,
Context: make(map[string]string),
}
}
// WithCause sets the underlying cause
func (e *AppError) WithCause(err error) *AppError {
e.Cause = err
return e
}
// WithDetail sets debug details
func (e *AppError) WithDetail(detail string) *AppError {
e.Detail = detail
return e
}
// WithContext adds context information
func (e *AppError) WithContext(key, value string) *AppError {
e.Context[key] = value
return e
}
// Predefined error codes
var (
ErrBadRequest = NewAppError(400, "bad request")
ErrUnauthorized = NewAppError(401, "unauthorized access")
ErrForbidden = NewAppError(403, "forbidden")
ErrNotFound = NewAppError(404, "resource not found")
ErrInternal = NewAppError(500, "internal server error")
)
// ==================== Simulated Business Layer ====================
// Request simulates an HTTP request
type Request struct {
UserID int
Path string
IsAdmin bool
}
// Response simulates an HTTP response
type Response struct {
StatusCode int
Body string
}
// validateRequest validates request parameters
func validateRequest(req *Request) error {
if req.UserID <= 0 {
return ErrBadRequest.
WithDetail("user_id must be a positive integer").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
if req.Path == "" {
return ErrBadRequest.
WithDetail("path cannot be empty")
}
return nil
}
// authenticate authentication
func authenticate(req *Request) error {
if req.UserID == 0 {
return ErrUnauthorized.WithDetail("missing authentication credentials")
}
return nil
}
// authorize authorization
func authorize(req *Request) error {
if !req.IsAdmin {
return ErrForbidden.
WithDetail("admin privileges required").
WithContext("user_id", fmt.Sprintf("%d", req.UserID))
}
return nil
}
// findResource finds a resource (may trigger panic)
func findResource(path string) (string, error) {
// Simulate a bug that causes panic
if path == "/crash" {
var p *int
*p = 1 // Nil pointer dereference, triggers panic
}
resources := map[string]string{
"/users": "User list",
"/profile": "User profile",
}
data, exists := resources[path]
if !exists {
return "", ErrNotFound.
WithDetail(fmt.Sprintf("resource for path %s not found", path))
}
return data, nil
}
// handleRequest processes a request (business logic)
func handleRequest(req *Request) (*Response, error) {
// 1. Validate parameters
if err := validateRequest(req); err != nil {
return nil, err
}
// 2. Authenticate
if err := authenticate(req); err != nil {
return nil, err
}
// 3. Authorize (only for admin paths)
if req.Path == "/admin" {
if err := authorize(req); err != nil {
return nil, err
}
}
// 4. Find resource
data, err := findResource(req.Path)
if err != nil {
return nil, err
}
return &Response{
StatusCode: 200,
Body: data,
}, nil
}
// ==================== Recovery Middleware ====================
// RecoveryMiddleware panic recovery middleware
func RecoveryMiddleware(handler func(*Request) (*Response, error)) func(*Request) (*Response, error) {
return func(req *Request) (resp *Response, err error) {
defer func() {
if r := recover(); r != nil {
// Log panic stack trace
stack := string(debug.Stack())
fmt.Printf("[PANIC] %v\nStack:\n%s\n", r, stack)
// Return internal error instead of crashing
err = ErrInternal.
WithDetail(fmt.Sprintf("panic: %v", r)).
WithCause(fmt.Errorf("panic: %v", r))
}
}()
return handler(req)
}
}
// ==================== Error Formatting ====================
// formatError formats an error into a friendly response
func formatError(err error) *Response {
var appErr *AppError
if errors.As(err, &appErr) {
msg := fmt.Sprintf("Error [%d]: %s", appErr.Code, appErr.Message)
if appErr.Detail != "" {
msg += fmt.Sprintf("\n Detail: %s", appErr.Detail)
}
if len(appErr.Context) > 0 {
msg += "\n Context:"
for k, v := range appErr.Context {
msg += fmt.Sprintf("\n %s: %s", k, v)
}
}
return &Response{
StatusCode: appErr.Code,
Body: msg,
}
}
return &Response{
StatusCode: 500,
Body: fmt.Sprintf("Unknown error: %v", err),
}
}
// ==================== Main Function ====================
func main() {
// Wrap handler with Recovery middleware
safeHandler := RecoveryMiddleware(handleRequest)
// Test various scenarios
testCases := []struct {
name string
req *Request
}{
{
name: "Normal request",
req: &Request{UserID: 1, Path: "/users", IsAdmin: true},
},
{
name: "Invalid parameters",
req: &Request{UserID: -1, Path: "/users"},
},
{
name: "Unauthenticated",
req: &Request{UserID: 0, Path: "/users"},
},
{
name: "Insufficient permissions",
req: &Request{UserID: 2, Path: "/admin", IsAdmin: false},
},
{
name: "Resource not found",
req: &Request{UserID: 1, Path: "/unknown"},
},
{
name: "Trigger panic (auto-recovery)",
req: &Request{UserID: 1, Path: "/crash"},
},
}
for _, tc := range testCases {
fmt.Printf("=== %s ===\n", tc.name)
resp, err := safeHandler(tc.req)
if err != nil {
resp = formatError(err)
}
fmt.Printf(" Status: %d\n", resp.StatusCode)
fmt.Printf(" Response: %s\n\n", resp.Body)
}
}
Output:
=== Normal request ===
Status: 200
Response: User list
=== Invalid parameters ===
Status: 400
Response: Error [400]: bad request
Detail: user_id must be a positive integer
Context:
user_id: -1
=== Unauthenticated ===
Status: 401
Response: Error [401]: unauthorized access
Detail: missing authentication credentials
=== Insufficient permissions ===
Status: 403
Response: Error [403]: forbidden
Detail: admin privileges required
Context:
user_id: 2
=== Resource not found ===
Status: 404
Response: Error [404]: resource not found
Detail: resource for path /unknown not found
=== Trigger panic (auto-recovery) ===
[PANIC] runtime error: invalid memory address or nil pointer dereference
Stack:
...
Status: 500
Response: Error [500]: internal server error
Detail: panic: runtime error: invalid memory address or nil pointer dereference
Real-World Scenarios
Scenario 1: Database Transaction Rollback
In database operations involving multiple steps, any failure requires rolling back already-executed operations.
package main
import (
"errors"
"fmt"
)
var (
ErrBalanceInsufficient = errors.New("insufficient balance")
ErrAccountFrozen = errors.New("account is frozen")
)
// TxError transaction error, records the failed step
type TxError struct {
Step string
Cause error
Actions []string // Executed actions that need rollback
}
func (e *TxError) Error() string {
return fmt.Sprintf("transaction failed [step: %s]: %v", e.Step, e.Cause)
}
func (e *TxError) Unwrap() error {
return e.Cause
}
// TransferService transfer service
type TransferService struct {
balances map[string]float64
frozen map[string]bool
}
// Transfer executes a transfer transaction
func (s *TransferService) Transfer(from, to string, amount float64) error {
var executedSteps []string
// Step 1: Validate source account
if s.frozen[from] {
return &TxError{
Step: "validate source account",
Cause: ErrAccountFrozen,
}
}
executedSteps = append(executedSteps, "lock source account")
// Step 2: Check balance
if s.balances[from] < amount {
return &TxError{
Step: "check balance",
Cause: ErrBalanceInsufficient,
Actions: executedSteps,
}
}
executedSteps = append(executedSteps, "balance check passed")
// Step 3: Deduct from source account
s.balances[from] -= amount
executedSteps = append(executedSteps, fmt.Sprintf("deducted %.2f", amount))
// Step 4: Validate target account
if s.frozen[to] {
return &TxError{
Step: "validate target account",
Cause: ErrAccountFrozen,
Actions: executedSteps, // Need to rollback deducted amount
}
}
// Step 5: Add to target account
s.balances[to] += amount
fmt.Printf(" Transfer successful: %s -> %s, Amount: %.2f\n", from, to, amount)
return nil
}
func main() {
svc := &TransferService{
balances: map[string]float64{
"Alice": 1000,
"Bob": 500,
"Carol": 0,
},
frozen: map[string]bool{
"Carol": true,
},
}
tests := []struct {
name string
from string
to string
amt float64
}{
{"Normal transfer", "Alice", "Bob", 200},
{"Insufficient balance", "Alice", "Bob", 9999},
{"Target account frozen", "Alice", "Carol", 100},
}
for _, t := range tests {
fmt.Printf("--- %s ---\n", t.name)
fmt.Printf(" Before: %s=%.2f, %s=%.2f\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
err := svc.Transfer(t.from, t.to, t.amt)
if err != nil {
fmt.Printf(" Failed: %v\n", err)
// Check if rollback is needed
var txErr *TxError
if errors.As(err, &txErr) && len(txErr.Actions) > 0 {
fmt.Printf(" Rollback %d executed actions:\n", len(txErr.Actions))
for _, action := range txErr.Actions {
fmt.Printf(" - Undo: %s\n", action)
}
// In real projects, execute rollback logic here
// e.g.: svc.balances[from] += amount
}
}
fmt.Printf(" After: %s=%.2f, %s=%.2f\n\n",
t.from, svc.balances[t.from], t.to, svc.balances[t.to])
}
}
Scenario 2: Batch File Processing with Error Collection
When processing multiple files, don't stop at the first failure. Instead, collect all errors and report them together.
package main
import (
"errors"
"fmt"
"strings"
)
var (
ErrFileCorrupted = errors.New("file is corrupted")
ErrPermission = errors.New("insufficient permissions")
ErrDiskFull = errors.New("disk space insufficient")
)
// FileError error for a single file
type FileError struct {
Path string
Op string
Cause error
}
func (e *FileError) Error() string {
return fmt.Sprintf("[%s] %s: %v", e.Op, e.Path, e.Cause)
}
func (e *FileError) Unwrap() error {
return e.Cause
}
// BatchResult batch processing result
type BatchResult struct {
Success []string
Failed []FileError
}
func (r *BatchResult) HasErrors() bool {
return len(r.Failed) > 0
}
func (r *BatchResult) Summary() string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("Processing complete: %d succeeded, %d failed\n",
len(r.Success), len(r.Failed)))
if len(r.Success) > 0 {
sb.WriteString("Successful files:\n")
for _, f := range r.Success {
sb.WriteString(fmt.Sprintf(" ✓ %s\n", f))
}
}
if len(r.Failed) > 0 {
sb.WriteString("Failed files:\n")
for _, e := range r.Failed {
sb.WriteString(fmt.Sprintf(" ✗ %s (%s: %v)\n", e.Path, e.Op, e.Cause))
}
}
return sb.String()
}
// processFile simulates processing a single file
func processFile(path string) error {
// Simulate various possible errors
fileErrors := map[string]error{
"corrupted.txt": ErrFileCorrupted,
"secret.txt": ErrPermission,
"huge.txt": ErrDiskFull,
}
if err, exists := fileErrors[path]; exists {
return &FileError{
Path: path,
Op: "process",
Cause: err,
}
}
return nil
}
// processFiles processes files in batch
func processFiles(paths []string) *BatchResult {
result := &BatchResult{}
for _, path := range paths {
err := processFile(path)
if err != nil {
result.Failed = append(result.Failed, FileError{
Path: path,
Op: "process",
Cause: errors.Unwrap(err),
})
} else {
result.Success = append(result.Success, path)
}
}
return result
}
func main() {
files := []string{
"report.pdf",
"data.csv",
"corrupted.txt",
"image.png",
"secret.txt",
"huge.txt",
"readme.md",
}
fmt.Printf("Starting to process %d files...\n\n", len(files))
result := processFiles(files)
fmt.Println(result.Summary())
// Provide different suggestions based on error types
for _, fe := range result.Failed {
switch {
case errors.Is(fe.Cause, ErrFileCorrupted):
fmt.Printf("Suggestion: %s needs to be restored from backup\n", fe.Path)
case errors.Is(fe.Cause, ErrPermission):
fmt.Printf("Suggestion: Please check file permissions for %s\n", fe.Path)
case errors.Is(fe.Cause, ErrDiskFull):
fmt.Printf("Suggestion: Free up disk space and retry %s\n", fe.Path)
}
}
}
❓ FAQ
Q1: When should I use panic vs return error?
Principle: Use error for expected failures; use panic for programming errors that "should never happen."
// ✓ Correct: return error — user input is unpredictable
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divisor cannot be zero")
}
return a / b, nil
}
// ✓ Correct: use panic — this is a programmer's bug, nil should never be passed
func MustNotNil(v interface{}) {
if v == nil {
panic("value must not be nil, this is a caller's programming error")
}
}
Practical rules:
- Return error: File operations, network requests, user input validation, business logic failures
- Use panic:
init()initialization failures, unrecoverable program errors,t.Fatalin tests - Never: Use
panicfor normal business errors
Q2: What's the difference between errors.Is and errors.As?
// errors.Is: checks if the error chain contains a specific sentinel error
// Equivalent to: err == target || err.Unwrap() == target || ...
errors.Is(err, ErrNotFound) // true/false
// errors.As: extracts a specific error type from the error chain
// Like a type assertion, but traverses the entire error chain
var appErr *AppError
errors.As(err, &appErr) // true + assignment, or false
// Analogy:
// errors.Is → "Are you Zhang San?" (checking identity)
// errors.As → "Are you a programmer? If so, tell me your skills" (extracting info)
Q3: Why are error messages recommended to start with lowercase and have no punctuation?
Go official and community convention:
// ✓ Recommended
fmt.Errorf("reading file %s failed: %w", name, err)
// Output: reading file config.yaml failed: permission denied
// ✗ Not recommended
fmt.Errorf("reading file %s failed.: %w", name, err) // Unnecessary punctuation
fmt.Errorf("reading file %s failed!: %w", name, err) // Unnecessary exclamation
fmt.Errorf("Read file %s failed: %w", name, err) // Inconsistent language
The reason is errors are often concatenated into error chains, and lowercase without punctuation is easier to read:
initializing database: connecting to postgres: dial tcp 127.0.0.1:5432: connection refused
Q4: How do I compare wrapped errors?
Direct == comparison won't work for wrapped errors; you must use errors.Is:
var ErrSentinel = errors.New("sentinel")
wrapped := fmt.Errorf("context: %w", ErrSentinel)
// ✗ Wrong: direct comparison fails
fmt.Println(wrapped == ErrSentinel) // false
// ✓ Correct: use errors.Is
fmt.Println(errors.Is(wrapped, ErrSentinel)) // true
// errors.Is traverses the entire error chain:
// wrapped → Unwrap() → ErrSentinel → match!
📖 Summary
| Topic | Key Points |
|---|---|
error interface |
Only needs to implement Error() string method |
| Creating errors | errors.New for simple errors, fmt.Errorf + %w for wrapped errors |
| Checking errors | errors.Is for type checking, errors.As for type extraction |
| Custom errors | Implement Error() + Unwrap() methods, include business context |
panic/recover |
Only for unrecoverable errors; catch with recover in defer |
| Error philosophy | Explicit handling, check each error, errors are values not exceptions |
| Error wrapping | Use %w to preserve underlying errors, use %v to discard them |
| Community convention | Error messages lowercase without punctuation, as the last return value |
Three golden rules of Go error handling:
- Check immediately — After receiving an
error, checkerr != nilright away - Wrap at each layer — Add context at each layer, use
%wto preserve the original error - Graceful degradation — Recover if possible, report up if not, panic only as a last resort
📝 Exercises
Exercise 1: Implement a File Reader with Retry
Write a function ReadWithRetry(path string, maxRetries int) ([]byte, error) with these requirements:
- Retry up to
maxRetriestimes - Print the retry count after each failure
- Define an
ErrMaxRetriesExceededsentinel error, returned when retries are exhausted - Use
fmt.Errorfto wrap underlying errors while preserving context
// Reference framework
var ErrMaxRetriesExceeded = errors.New("max retries exceeded")
func ReadWithRetry(path string, maxRetries int) ([]byte, error) {
var lastErr error
for i := 0; i < maxRetries; i++ {
data, err := os.ReadFile(path)
if err == nil {
return data, nil
}
lastErr = err
fmt.Printf(" Retry %d failed: %v\n", i+1, err)
}
return nil, fmt.Errorf("reading %s: %w (last error: %v)", path, ErrMaxRetriesExceeded, lastErr)
}
Exercise 2: Build an Error Level System
Implement an error level system supporting the following error levels:
type Level int
const (
LevelDebug Level = iota
LevelInfo
LevelWarn
LevelError
LevelFatal
)
type LeveledError struct {
Level Level
Message string
Cause error
}
Requirements:
- Implement the
errorinterface - Implement the
Unwrap() errormethod - Write an
IsLevel(err error, level Level) boolfunction - Test level checking for various errors
Exercise 3: Implement an Error Collector (Concurrency Safe)
Write an ErrorCollector type for collecting errors from multiple goroutines in concurrent scenarios:
type ErrorCollector struct {
// Fields you need
}
// Add adds an error (concurrency safe)
func (c *ErrorCollector) Add(err error) { ... }
// Errors returns all collected errors
func (c *ErrorCollector) Errors() []error { ... }
// HasErrors checks if there are any errors
func (c *ErrorCollector) HasErrors() bool { ... }
// Error merges all errors into a single error
func (c *ErrorCollector) Error() error { ... }
Requirements:
- Use
sync.Mutexorsync.RWMutexfor concurrency safety - Write tests: launch multiple goroutines calling
Addconcurrently, then verify the collection results
Next Lesson
After completing this lesson, continue with Lesson 11: Packages and Modules to learn how to organize Go code, manage dependencies, and publish your own packages.



