The Secret Life of Go: Error Handling (Part 3)
The Secret Life of Go: Error Handling (Part 3)
Behavioral errors, interfaces, and the retry loop
#Golang #ErrorHandling #SoftwareArchitecture #BackendDev
Eleanor is a senior software engineer. Ethan is her junior colleague. They work in a beautiful beaux arts library in Lower Manhattan — the kind of place where coding languages are discussed like poetry.
Episode 32
Ethan was writing a background worker that synced data with a notoriously flaky external API. He had written a loop to retry the request if it failed, but the if statement was getting out of hand.
"Look at this monster," Ethan said, pointing at his screen as Eleanor walked by with her tea.
// Ethan's brittle retry logic
response, err := externalAPI.FetchData()
if err != nil {
// Check every possible network failure identity
if errors.Is(err, network.ErrTimeout) ||
errors.Is(err, network.ErrConnectionReset) ||
errors.Is(err, network.ErrDNSTimeout) {
fmt.Println("Network glitch, retrying in 5 seconds...")
time.Sleep(5 * time.Second)
continue
}
return fmt.Errorf("fatal API error: %w", err)
}
Eleanor studied the code. "You are tightly coupling your business logic to specific network implementations. What happens when we swap out the HTTP client, or the networking package adds a new ErrTLSHandshakeTimeout?"
"I have to come back and add a fourth errors.Is to this giant chain," Ethan sighed. "I'm exhausting the identity check we learned in Episode 30."
"Exactly," Eleanor nodded. "Identity checks—asking 'What is this error?'—are great for exact matches like ErrNotFound. But for broad categories like network blips, you shouldn't ask what the error is. You should ask what the error can do."
The Behavioral Interface
Eleanor pulled up the file where Ethan defined his custom error structs from Episode 31.
"Instead of making the caller guess every possible struct or Sentinel that might represent a temporary failure, we define a behavior," she explained.
She typed out a tiny, one-method interface:
// Temporary describes any error that indicates a transient failure.
type Temporary interface {
Temporary() bool
}
"Now," Eleanor said, "any custom error struct in our entire application—whether it's a database timeout, a network drop, or a rate limit—can silently implement this interface just by adding a Temporary() bool method."
// A custom error struct from the network package
type TimeoutError struct {
URL string
Err error
}
func (e *TimeoutError) Error() string { return "timeout reaching " + e.URL }
func (e *TimeoutError) Unwrap() error { return e.Err }
// Implement the behavioral interface!
func (e *TimeoutError) Temporary() bool { return true }
The errors.As Magic Trick
Ethan looked back at his massive retry loop. "Okay, so the underlying errors now have a Temporary() method. But they are still hidden deep inside wrapped error chains. Do I have to unwrap them and type-assert every single one to the Temporary interface?"
"No," Eleanor smiled. "You already know the magic spell for searching a wrapped error chain. We use errors.As."
She deleted his giant if statement and rewrote it in three lines:
response, err := externalAPI.FetchData()
if err != nil {
// 1. Declare a variable of the INTERFACE type
var tempErr Temporary
// 2. Ask Go: "Does ANY error in this wrapped chain implement
// the Temporary interface? If so, load it into tempErr."
if errors.As(err, &tempErr) && tempErr.Temporary() {
fmt.Println("Transient failure, retrying in 5 seconds...")
time.Sleep(5 * time.Second)
continue
}
return fmt.Errorf("fatal API error: %w", err)
}
Ethan's jaw dropped slightly. "errors.As works on interfaces too?"
"It does," Eleanor confirmed. "It doesn't just look for concrete structs. It will traverse the entire Russian nesting doll of errors, asking each one: Do you have a Temporary() method? The moment it finds one, it populates your interface variable and returns true."
The Architect's Lesson
Ethan leaned back, taking in the refactored code. "This changes everything. My worker function doesn't need to import the network package, the database package, or the rate-limiter package. It doesn't care who generated the error or what the error is."
"Decoupling at its finest," Eleanor said. "The producer of the error provides the context. The consumer of the error only asserts for the behavior it cares about. You have completely separated your retry logic from your network implementations."
Ethan deleted the old, brittle code. He had moved from checking strings, to checking identities, to checking data, and finally, to asserting behaviors. His error handling was no longer a liability. It was an architecture.
Key Concepts from Episode 32
Identity vs. Behavior
- Identity (
errors.Is): Asks "Are you exactly this specific error?" (e.g.,ErrNotFound). Good for specific, narrow checks. - Behavior (Interfaces): Asks "Can you do this specific thing?" (e.g.,
Temporary()). Good for broad categories of errors where the exact underlying type doesn't matter.
Interfaces as Error Contracts
- You can define small, focused interfaces (like
interface { Timeout() bool }orinterface { RetryAfter() time.Duration }) to standardize how different packages communicate failure states.
errors.As with Interfaces
errors.Asis not limited to concrete structs. If you pass a pointer to an interface variable (&tempErr), Go will unwrap the error chain and find the first error that satisfies that interface's method set.
Aaron Rose is a software engineer and technology writer at tech-reader.blog.
Catch up on the latest explainer videos, podcasts, and industry discussions below.


Comments
Post a Comment