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
26: Error Wrapping with fmt.Errorf and errors.Is/As
I see this happen all the time when developers move from simple scripts to larger Go projects. You start wrapping errors to provide context—which is exactly what you should be doing—but then you realize your error handling logic has mysteriously stopped working. Let's look at a typical example.
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("resource not found")
func findUser(id int) error {
// Simulate a database miss
return ErrNotFound
}
func getUserProfile(id int) error {
err := findUser(id)
if err != nil {
// We want to add context so we know where the error happened
return fmt.Errorf("getUserProfile failed: %v", err)
}
return nil
}
func main() {
err := getUserProfile(123)
if err == ErrNotFound {
fmt.Println("Handle 404: User not found")
} else {
fmt.Printf("Unexpected error: %v\n", err)
}
}
The disappearing error identity
If you run the code above, you'll notice it prints Unexpected error: getUserProfile failed: resource not found instead of the 404 handler. Why? Because fmt.Errorf with the %v verb creates a brand new error string. It's a new value entirely. Even though the text looks the same, the original ErrNotFound sentinel is gone, swallowed by the new string. You've effectively "blinded" your calling function to the original cause of the failure.
Preserving the chain with %w
To fix this, Go provides the %w verb. The 'w' stands for wrap. When you use %w, Go doesn't just format a string; it creates a wrapper that maintains a link to the original error. This creates an "error chain" that we can traverse later.
But simply changing %v to %w isn't enough. You also have to stop using the == operator. Because the error is now wrapped, it's no longer equal to the sentinel; it's a wrapper containing the sentinel. This is where errors.Is comes in.
func getUserProfile(id int) error {
err := findUser(id)
if err != nil {
// Notice the %w here instead of %v
return fmt.Errorf("getUserProfile failed: %w", err)
}
return nil
}
func main() {
err := getUserProfile(123)
// Use errors.Is to walk the chain and look for ErrNotFound
if errors.Is(err, ErrNotFound) {
fmt.Println("Handle 404: User not found")
}
}
I always tell people to think of errors.Is as "Is this error, or any error in its history, equal to this value?"
Extracting metadata with errors.As
Sometimes you don't just need to know if an error happened, but you need specific data from a custom error type. errors.Is is great for sentinels (simple values), but for structs, you want errors.As.
Imagine your database driver returns a QueryError that contains the specific SQL state code. You can't use errors.Is because every QueryError is a different instance. Instead, errors.As lets you say: "Is there any error in this chain that is of this specific type? If so, please assign it to this variable."
type QueryError struct {
Query string
Code int
}
func (e *QueryError) Error() string {
return fmt.Sprintf("query %q failed with code %d", e.Query, e.Code)
}
func runQuery() error {
return fmt.Errorf("db layer: %w", &QueryError{Query: "SELECT...", Code: 5432})
}
func main() {
err := runQuery()
var qErr *QueryError
// If a *QueryError exists anywhere in the chain, it's assigned to qErr
if errors.As(err, &qErr) {
fmt.Printf("Database error code: %d\n", qErr.Code)
}
}
One quick tip: always pass a pointer to the pointer (&qErr) when using errors.As. It feels a bit clunky at first, but it's how the any type internal to the function can actually modify your variable.
📋 Practical Task
Exercise: Implementing a Tiered API Error Handler
You are building a system that fetches weather data. You need to implement a chain of errors that allows a top-level handler to distinguish between a temporary network glitch and a permanent configuration error, even after the errors have been wrapped with context.
Requirements:
- Create two sentinel errors:
ErrNetworkTimeoutandErrInvalidAPIKey. - Create a custom struct
APIErrorthat includes a fieldStatusCode intand implements theError()method. - Write a function
fetchWeather() errorthat returns aQueryErrorwrapped inside a generic error usingfmt.Errorfand%w. - Write a function
validateKey() errorthat returnsErrInvalidAPIKeywrapped inside another error. - In
main(), call these functions and useerrors.Isto detect theErrInvalidAPIKeyanderrors.Asto extract theStatusCodefrom theAPIError.
Your final code should demonstrate that you can identify the root cause of the error regardless of how many layers of context have been added by fmt.Errorf.
There are no comments for now.