-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions and Methods
-
Section 4: Concurrency
-
Section 5: Packages and Tooling
-
Section 6: More Standard Library
-
Section 7: Building Services
-
Section 8: Advanced Go
-
Section 9: Go in the Cloud-Native Ecosystem
-
Section 10: Data Structures and Algorithms in Go
-
Section 11: Testing and Deployment
-
Section 12: Practical Projects
-
Section 13: More Standard Library Practice
-
Section 14: More Practice Projects
-
Section 15: Design Patterns in Go
-
Section 16: Interview Practice
-
Section 17: Package fmt In Depth
-
Section 18: Package strings and strconv
-
Section 19: Package os and io
-
Section 20: Package time
-
Section 21: Package sort and container
-
Section 22: Package encoding
-
Section 23: Package net/http In Depth
-
Section 24: Package context
-
Section 25: Package regexp and bytes
-
Section 26: Package errors In Depth
-
Section 27: Package crypto and hash
-
Section 28: Package flag and log
-
Section 29: Package sync In Depth
-
Section 30: More Practice Exercises
-
Section 31: Go Modules and Workspaces In Depth
-
Section 32: Generics Deep Dive (Go 1.18+)
-
Section 33: Testing Package In Depth
-
Section 34: More Interview and Whiteboard Practice
-
Section 35: Package math and unicode
-
Section 36: Package path and filepath
-
Section 37: Package database/sql
-
Section 38: Package text/template and html/template
-
Section 39: Package archive and compress
-
Section 40: Lower-Level net Package
-
Section 41: Package runtime and reflect
-
Section 42: Package embed and io/fs
-
Section 43: Go Assembly and CGO Basics
-
Section 44: Building CLIs and TUIs
-
Section 45: Go Performance Tuning
-
Section 46: More Real-World Projects
-
Section 47: Go in Production
-
Section 48: Go Design Patterns
-
Section 49: Go Interfaces Deep Dive
-
Section 50: Final Practice and Review
28: Practice Exercise: Building a Custom Error Handling Strategy
I've spent a lot of time in production systems where the logs are just filled with "something went wrong." It's the bane of my existence. In Go, the simple if err != nil pattern is great for flow control, but it doesn't tell us why something failed or how we should react to it. Let's build a payment processing module and see where the basic approach falls apart.
The fragility of string matching
Initially, I might just throw some errors out there using errors.New(). Let's look at this quick snippet I wrote for a mock payment gateway:
func ProcessPayment(amount float64) error {
if amount <= 0 {
return errors.New("invalid amount")
}
if amount > 1000 {
return errors.New("insufficient funds")
}
return nil
}
// At the call site...
err := ProcessPayment(1200)
if err != nil && err.Error() == "insufficient funds" {
fmt.Println("Asking user for more money...")
}
This works... until it doesn't. What happens if I change the error message to "insufficient funds in account" for better UX? My if statement breaks silently. I'm relying on a string, which is basically a promise that I'll never change a typo. That's a recipe for a 3 AM pager call.
Trying sentinel errors
To fix this, I'll use sentinel errors. I'll define them as package-level variables so the caller can compare them directly. It feels cleaner.
var (
ErrInvalidAmount = errors.New("invalid amount")
ErrInsufficientFunds = errors.New("insufficient funds")
)
func ProcessPayment(amount float64) error {
if amount <= 0 {
return ErrInvalidAmount
}
if amount > 1000 {
return ErrInsufficientFunds
}
return nil
}
Now I can use errors.Is(err, ErrInsufficientFunds). Much better. But wait—what if I want to tell the user how much they are short? A sentinel error is a static value; I can't attach the current balance or the required amount to it without creating a new error every time, which puts us right back at the string-matching problem.
Building a typed error system
This is where I usually pivot to custom error types. By defining a struct, I can carry metadata along with the error. I'll create a specific type for payment failures.
type PaymentError struct {
AmountRequested float64
CurrentBalance float64
Code int
}
func (e *PaymentError) Error() string {
return fmt.Sprintf("payment failed: requested %.2f, but balance is %.2f (code: %d)",
e.AmountRequested, e.CurrentBalance, e.Code)
}
Now, instead of returning a generic error, I can return this struct. But there's a catch. If I wrap this error in another layer of context (like fmt.Errorf("checkout failed: %w", err)), a simple type assertion like err.(*PaymentError) will fail because the error is now wrapped in a different internal Go type.
The power of errors.As
I've learned the hard way that in modern Go, you should almost never use type assertions for errors. Instead, I use errors.As. It recursively unwraps the error chain until it finds a match for the target type.
Let's see this in action with a wrapper function that simulates a high-level checkout process:
func Checkout(amount float64) error {
err := ProcessPayment(amount)
if err != nil {
// We wrap the error to add context, but keep the original error inside
return fmt.Errorf("checkout process failed: %w", err)
}
return nil
}
func main() {
err := Checkout(1200)
var payErr *PaymentError
if errors.As(err, &payErr) {
fmt.Printf("User is short by %.2f\n", payErr.AmountRequested - payErr.CurrentBalance)
} else {
fmt.Println("A non-payment error occurred")
}
}
By using %w in fmt.Errorf and errors.As at the call site, I've built a system that is both descriptive for the logs (thanks to the wrapping) and programmatically actionable for the logic (thanks to the custom struct). We've moved from fragile strings to a robust, type-safe strategy.
📋 Practical Task
Exercise: Implementing a Multi-Tiered Order Validation Pipeline
You are building an e-commerce order validation system. You need to create a custom error handling strategy that distinguishes between different types of validation failures so the frontend can display different UI alerts.
- Create a custom error struct called
ValidationErrorthat includes aField(string) and aReason(string). - Implement the
Error()method forValidationError. - Create a function
ValidateOrder(order Order) error. If theQuantityis less than 1, return aValidationErrorfor the "Quantity" field. If theSKUis empty, return aValidationErrorfor the "SKU" field. - Create a wrapper function
ProcessOrder(order Order) errorthat callsValidateOrderand wraps any returned error usingfmt.Errorfwith the%wverb to add the prefix "Order Processing Error: ". - In your
mainfunction, callProcessOrderwith an invalid order, and useerrors.Asto extract theValidationErrorand print only theFieldthat caused the failure.
There are no comments for now.