How it works
The explanation, with examples you can run.
Wrapping and inspecting
package main import ( "errors" "fmt" "io/fs" "os") var ErrNoConfig = errors.New("no config") func loadConfig(path string) error { _, err := os.ReadFile(path) if errors.Is(err, fs.ErrNotExist) { return fmt.Errorf("load %s: %w", path, ErrNoConfig) } if err != nil { return fmt.Errorf("load %s: %w", path, err) } return nil} func main() { err := loadConfig("/nope/app.toml") fmt.Println(err) fmt.Println(errors.Is(err, ErrNoConfig)) _, err = os.Open("/nope/app.toml") err = fmt.Errorf("startup: %w", err) var pe *fs.PathError if errors.As(err, &pe) { fmt.Println("op:", pe.Op, "path:", pe.Path) } if pe, ok := errors.AsType[*fs.PathError](err); ok { // Go 1.26+ fmt.Println("same with AsType:", pe.Err) }}This prints load /nope/app.toml: no config, true, op: open path: /nope/app.toml, and same with AsType: no such file or directory.
- Sentinel errors such as
ErrNoConfigorio.EOFare package-level values. Check them witherrors.Is, which followsUnwrapthrough every layer. - Error types such as
*fs.PathErrorcarry fields.errors.Asfinds the first error in the chain that has that type and stores it in your variable. Go 1.26 addederrors.AsType, a generic version that returns the value directly and is checked at compile time. %wmakes the result unwrappable;%vonly copies the text. Wrapping exposes the inner error as part of your API, so wrap when callers should be able to depend on it.errors.Join(Go 1.20) combines several errors into one;IsandAscheck each of them.
Adding context
Each layer should say what it was doing: fmt.Errorf("charge order %s: %w", id, err). Read from the top, the message becomes a trail such as charge order 42: call payments: context deadline exceeded. Don't log and return the same error: it gets reported twice. Handle it once, at the level that can do something about it.
panic and recover
panic stops the normal flow, runs deferred calls as it unwinds the goroutine's stack, and crashes the program with a stack trace if nothing calls recover. recover only works inside a deferred function. Reserve panics for bugs (an index out of range, a nil map write, an impossible case) and for failures during start-up. net/http recovers a panic in a handler and logs it, but a panic in a goroutine you started yourself takes down the whole process. A library shouldn't let a panic escape its API.
Deferred calls also run on a normal return, which is why defer f.Close() right after a successful Open is the usual way to release resources.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.