Language Internals
Part 5 of 11 · Go Language ProficiencyErrors, Panics & Wrapping
Errors as values, wrapping, errors.Is and errors.As, and panic or recover at a boundary.
- 1Gist
- 2Maps
- 3Q&A
- 4Sandbox
Voice readout needs Web Speech Synthesis in this browser.
Question ladder
L1
What does a failing function return?
Answer
A zero value and a non-nil error. The caller checks err before using the value.
L2
How do you add context and keep the cause?
Answer
fmt.Errorf with the percent-w verb. errors.Is and errors.As walk that chain.
L3
What does percent-v do instead?
Answer
It formats the error text and drops the chain. Is and As cannot see the original.
L4
Is or As?
Answer
Is matches a sentinel value. As matches a type, such as a path error, into a pointer you pass.
L5
Where may recover run?
Answer
Inside a function that was deferred. It sees the panic from the same goroutine.
L6
What belongs in a library?
Answer
Returned errors. Panic only for a bug, such as a violated invariant you already documented.
L7
How is that different from except Exception?
Answer
A wide except hides the failure type. Go's check is local, and wrapping keeps the type available upstairs.
Failure modes
Dropped error
The blank identifier takes err and the zero value is treated as success.
percent-v wrap
The log line looks wrapped, but errors.Is no longer matches the sentinel.
recover in the helper
A library swallows a panic and returns nil, so the bug disappears.
Typed nil error
A nil pointer of an error type is stored in an error interface and looks non-nil.
Misconceptions
panic is Go's exception system.
Ordinary failure returns error. panic means the process hit a bug or a boundary must convert a crash into a 500.
Any format verb wraps.
Only the percent-w verb wraps. percent-v does not.
recover works anywhere in the function.
It must run directly in a deferred function on that goroutine.
errors.Is is a string compare.
It walks the unwrap chain for a sentinel value.
Interviewer traps
Wrapping with percent-v and still calling Is.
Name the percent-w verb and show Is matching the sentinel.
Putting recover next to business validation.
Return ErrMissing. recover stays in middleware.
Comparing error text.
Use Is for sentinels and As for types.
Design scenario
Same prompt for every reader.
Requirements
A sentinel for empty input, wrap the os error, Is for the sentinel, As for a path error.
Traffic / scale
Startup and a reload endpoint.
Latency
The check is local. A panic unwind is the expensive and wrong path.
Consistency
Callers must be able to distinguish missing from empty after two wraps.
Availability
An unrecovered panic on a request goroutine takes down that request and, if it escapes, the process.
Failure assumptions
- Validation panics and middleware is the only parser.
- The wrap uses percent-v so the sentinel check fails.
Constraints
- Do not build a framework error type.
- Say where the one recover lives.
Prompt
Load a config file and tell a missing file from an empty file without panicking.
A value versus an unwind
Prefer
error in the signature
The next line handles it or wraps it. Is and As still work after a wrap.
- Sentinels use Is.
- Types use As.
- Panic is a bug signal.
Alternative
throw and raise
TypeScript and Python unwind until a catch. The signature can look total.
- cause and raise-from keep a chain too.
- A wide catch hides the type.
- Control flow leaves the function invisibly.
Overview
Check err != nil. Add context with fmt.Errorf and the %w verb. Match sentinels with errors.Is and concrete types with errors.As. Reserve panic for invariants, and recover for the edge of a request or a goroutine.
Decisions
- 1
Call fails
- nextWrap and return
- 2
Wrap and return
- nextKnown sentinel?
- ?
Known sentinel?
- yeserrors.Is
- noerrors.As or stop
- 4
errors.Is
- 5
errors.As or stop
Lesson map
Errors, Panics & Wrapping
Errors as values, wrapping, errors.Is and errors.As, and panic or recover at a boundary.
Architecture. Call fails Ready. Wrap and return Ready. Known sentinel? Ready. errors.Is Ready. errors.As or stop Ready
Select a node to see why it exists, or an edge to see the protocol, direction, effect, and consequence.
Mermaid export
flowchart TB Call["Call fails Ready"] Wrap["Wrap and return Ready"] Known["Known sentinel? Ready"] IsNode["errors.Is Ready"] AsNode["errors.As or stop Ready"] Call -->|continues| Wrap Wrap -->|continues| Known Known -->|yes| IsNode Known -->|no| AsNode
Press Run. Snippets must be self-contained — no network, files, or native modules.
Python's raise ... from keeps a cause the way %w keeps a chain. The Go caller still has to look.
Rosetta — wrap and match
package config
import (
"errors"
"fmt"
"os"
)
var ErrMissing = errors.New("missing config")
func Load(path string) ([]byte, error) {
b, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load %s: %w", path, err)
}
if len(b) == 0 {
return nil, ErrMissing
}
return b, nil
}
func Use(path string) error {
_, err := Load(path)
if errors.Is(err, ErrMissing) {
return fmt.Errorf("config required: %w", err)
}
var pe *os.PathError
if errors.As(err, &pe) {
return fmt.Errorf("path %s: %w", pe.Path, err)
}
return err
}export class MissingConfigError extends Error {}
export async function load(path: string): Promise<Uint8Array> {
let buf: Uint8Array;
try {
buf = await Deno.readFile(path);
} catch (err) {
throw new Error(`load ${path}`, { cause: err });
}
if (buf.byteLength === 0) throw new MissingConfigError("missing");
return buf;
}
export async function use(path: string): Promise<Uint8Array> {
try {
return await load(path);
} catch (err) {
if (err instanceof MissingConfigError) return new TextEncoder().encode("defaults");
throw err;
}
}class MissingConfigError(Exception):
pass
def load(path: str) -> bytes:
try:
with open(path, "rb") as handle:
data = handle.read()
except OSError as exc:
raise RuntimeError(f"load {path}") from exc
if not data:
raise MissingConfigError("missing")
return data%w is the wrap. %v only prints. TypeScript's cause and Python's raise from are the nearest analogs, and both still throw.
Rosetta — recover at the boundary
package httpx
import (
"log"
"net/http"
)
func RecoverMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
log.Printf("panic: %v", v)
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}function recoverMiddleware(err: unknown, _req: unknown, res: { status(code: number): { send(body: string): void } }) {
console.error(err);
res.status(500).send("internal server error"); // log err, never echo it to clients
}async def all_exc(_request: object, exc: Exception) -> dict[str, str]:
return {"error": str(exc)}recover sees a panic only in the deferred function on that goroutine. A library should return ErrMissing, not panic, when the file is empty.
Interview Q&A
percent-v or percent-w?
Answer
percent-w wraps so Is and As still walk the chain. percent-v stringifies and the sentinel disappears.
Is or As?
Answer
Is for a sentinel you created with errors.New. As for a type such as os.PathError.
Where does recover belong?
Answer
In a deferred function at an HTTP middleware boundary or at the top of a goroutine you started. Not beside validation.
Does recover stop a panic on another goroutine?
Answer
No. Each goroutine needs its own deferred recover, or the process crashes.
Why can a nil pointer fail an err != nil check?
Answer
The interface holds a type. A typed nil is not a nil interface. Return nil without a concrete type.
How is Python raise from different?
Answer
It chains exceptions and still unwinds. The caller might not declare the failure. Go's caller has a result to check.
How is TypeScript cause different?
Answer
Error's cause is also a chain, but the function type often still says the happy type. The throw is a side channel.
Did Go 1.27 replace this model?
Answer
No. 1.27.1 still uses error values, percent-w, Is, and As. Panic is still the wrong tool for a bad config.
Pitfalls
_ = erron a write.- Wrapping with
%vand wondering whyerrors.Isfails. recoverin a helper that returns nil.- Panicking across a package boundary that should have returned an error.
Take a function that reads a file. Name the sentinel, the wrap verb, and the single function that may recover.