20.8 When to Handle vs When to Return Errors

Right, let’s talk about the single most common decision you’ll make when writing Go: do you handle this error right here, or do you kick this problem up the chain for someone else to deal with? This isn’t just academic; getting this wrong is how you end up with either fragile code that crashes on the first hiccup or a sprawling mess of if err != nil blocks that obscure your actual logic.

20.7 Custom Error Types: Adding Structured Information

Right, let’s talk about making your errors actually useful. The built-in error interface is brilliantly simple, but let’s be honest, a string wrapped in an interface is about as informative as a “Something Went Wrong” alert on a vending machine that just ate your last dollar. You know a problem exists, but you have no idea why or what to do about it. That’s where custom error types come in. We’re going to move beyond “file not found” to “file ‘go.mod’ not found in /home/you/project: permission denied”.

20.6 errors.As: Extracting a Specific Error Type from the Chain

Right, so you’ve wrapped an error, and now you’ve got this whole chain of them. It’s like a Russian nesting doll of failure. You know somewhere deep inside this mess is a specific type of error you actually care about—maybe a *os.PathError to check which file it choked on, or a custom TemporaryError you’ve defined to see if you should retry the operation. You could try to manually peel back the layers with a series of errors.Is checks and type assertions, but that’s tedious, error-prone, and frankly, a bit ugly. Go’s designers, in their infinite wisdom (a phrase I use with the same sincerity as “interesting weather we’re having”), gave us a better tool: errors.As. This function is your surgical extractor for specific error types from within a chain.

20.5 errors.Is: Checking Error Identity Through Wrapped Chains

Alright, let’s talk about errors.Is. This is where Go’s error handling graduates from “well, it’s simple” to “oh, actually, that’s quite clever.” You’ve probably been there: you get an error, you unwrap it, you start doing == checks or peeking at its message like a detective at a crime scene. It’s clunky, brittle, and frankly, a bit amateur hour. The errors.Is function is your ticket out of that mess. Think of it as a bloodhound that can sniff its way through a whole chain of wrapped errors to find a specific target. It doesn’t just check the error on the surface; it recursively unwraps the entire error chain, looking for a match. This is the idiomatic, robust way to check for specific types of errors in Go.

20.4 Error Wrapping with %w and fmt.Errorf

Right, let’s talk about wrapping errors. This is where we go from the polite but utterly useless “something went wrong” to the glorious, detailed “the flux capacitor failed because you tried to input 1.21 gigawatts on a 15-amp household circuit, you maniac.” Before Go 1.13, we were all basically doing this by hand, attaching context with fmt.Errorf("some context: %v", err). It worked, but it was a string-concatenation free-for-all. There was no standard way to unwrap the error and get back to the original cause. The %w verb and the accompanying errors package changes fixed that. It’s one of those “why wasn’t it always like this?” features.

20.3 Sentinel Errors: errors.New() and Exported Error Values

Right, let’s talk about sentinel errors. This is the part where we graduate from just returning fmt.Errorf("something broke") and start building an error handling strategy that doesn’t suck. The name sounds fancy, but the concept is simple: a sentinel error is a predefined, exported (public) error value that you can check against. Think of them as unique error constants, like little flags your code can raise to signal specific, well-known problems.

20.2 Returning Errors: The (value, error) Convention

Alright, let’s talk about the single most brilliant and simultaneously most annoying piece of Go syntax you’ll encounter: (value, error). It’s the backbone of how we handle things going wrong, and it’s so ingrained in the language’s DNA that you’ll feel its absence when you go back to languages that just chuck exceptions around like confetti. The core idea is devastatingly simple: any function that can fail should return both the thing you wanted and a separate, explicit error value. If the function succeeded, you get your value and a nil error. If it failed, you get a zero-value (or whatever partial result was achieved) and a non-nil error describing what went pear-shaped. This isn’t a suggestion; it’s a convention so strong it might as well be law. The compiler won’t yell at you if you don’t do it, but every other Go programmer will.

20.1 The error Interface: Error() string

Right, let’s talk about error. It’s the one interface you’ll use more than any other, and its design is so stupidly simple it’s almost offensive. Here’s the entire definition, straight from the source: type error interface { Error() string } That’s it. No GetMessage(), no GetStatusCode(), no GetUnderlyingCause(). Just a single method that returns a string. When the Go designers landed on this, I imagine there were high-fives all around. They had achieved maximum simplicity. It’s brilliant because it’s minimal, and it’s infuriating for the exact same reason. But before we get mad, let’s understand the genius in the constraint.

— joke —

...