Go
Completed
-
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
150: Wrapping Errors with %w
A few years ago, I was reviewing a PR for a teammate who was building a complex payment processing pipeline. They had a deep call stack: the API handler called a service, which called a repository, which finally called the database driver. When an error happened at the bottom, they were using fmt.Errorf("payment failed: %v", err) to pass the error back up the chain. It looked fine in the logs, but we hit a wall when we tried to implement a retry mechanism. We needed to know if the error was a temporary network glitch (retryable) or a validation error (non-retryable). Because they used %v, the original error was converted into a plain string. The "type" of the error was gone, and we were stuck trying to do fragile string matching on error messages to decide whether to retry. That's a nightmare you want to avoid.
Adding Context without Obscuring the Root Cause
In Go, when you want to add a bit of helpful context to an error—like which user ID caused the failure or which file couldn't be opened—you have two main choices. If you use %v or %s in fmt.Errorf, you're essentially creating a brand new error that contains the old error's message as a string. You've effectively "flattened" the error.
The %w verb changes the game. It stands for "wrap." When you use %w, Go doesn't just copy the string; it creates a wrapper that holds a reference to the original error. This allows you to build a chain of errors, adding a layer of context at every level of your architecture while keeping the original root cause intact.
package main
import (
"errors"
"fmt"
)
var ErrDatabaseDown = errors.New("database is offline")
func connectDB() error {
// Simulating a low-level failure
return ErrDatabaseDown
}
func initializeApp() error {
err := connectDB()
if err != nil {
// Wrap the error with %w to preserve ErrDatabaseDown
return fmt.Errorf("app initialization failed: %w", err)
}
return nil
}
func main() {
err := initializeApp()
fmt.Println(err)
// Output: app initialization failed: database is offline
}
Unwrapping the Chain with errors.Is
Now, here is where the magic actually happens. Because we used %w, the error returned by initializeApp isn't just a string—it's a wrapper. We can use the errors.Is function to "peer through" all the layers of wrapping to see if a specific error exists anywhere in that chain.
If I had used %v in the example above, errors.Is(err, ErrDatabaseDown) would return false because the original error was destroyed. With %w, it returns true. I personally find this indispensable for writing clean boundary logic: your deep internal packages can define sentinel errors, and your top-level handlers can check for them without needing to know exactly how many layers of wrapping occurred in between.
// Continuing from the previous example...
func main() {
err := initializeApp()
if errors.Is(err, ErrDatabaseDown) {
fmt.Println("Alert: The database is down! Triggering failover...")
} else {
fmt.Println("An unknown error occurred.")
}
}
One quick word of caution: don't wrap everything blindly. Wrapping is for when the caller actually needs to programmatically react to the underlying cause. If you're just logging a message for a human to read and the error type doesn't matter, %v is perfectly fine. But the moment you think, "I might need to check if this was a TimeoutError later," reach for %w.
📋 Practical Task
Refactoring the File Storage Error Chain
You are working on a cloud storage library. Currently, the code is using %v to pass errors up, which is preventing the main application from detecting specific failure types. Your goal is to refactor the error handling to use %w so that the main function can identify when a PermissionDenied error has occurred.
Requirements:
- Modify
readDiskFileto wrap the sentinel error using%w. - Modify
loadConfigurationto wrap the error fromreadDiskFileusing%w. - Update the
mainfunction to useerrors.Isto check if the resulting error isErrPermissionDeniedand print "Access Denied: Please check your system permissions" if it is.
package main
import (
"errors"
"fmt"
)
var ErrPermissionDenied = errors.New("permission denied")
func readDiskFile(path string) error {
// Simulating a permission error from the OS
return fmt.Errorf("disk read failed for %s: %v", path, ErrPermissionDenied)
}
func loadConfiguration() error {
err := readDiskFile("/etc/config.yaml")
if err != nil {
return fmt.Errorf("could not load config: %v", err)
}
return nil
}
func main() {
err := loadConfiguration()
if err != nil {
// TODO: Use errors.Is to check for ErrPermissionDenied
fmt.Printf("Error: %v\n", err)
}
}
There are no comments for now.