Irbisa · Cheatsheet
Errors & Panics
error values, wrapping with %w, errors.Is/As, panic and recover
How does error handling work in Go: what is the
errortype, and why does Go return errors instead of throwing exceptions?func parsePort(s string) (int, error) { n, err := strconv.Atoi(s) if err != nil { return 0, fmt.Errorf("parse port %q: %w", s, err) } if n < 1 || n > 65535 {…What is a sentinel error, how do you declare and compare one, and why is
errors.New("x") == errors.New("x")false?var ErrUserNotFound = errors.New("user not found") func main() { a, b := errors.New("user not found"), errors.New("user not found") fmt.Println(a == b) // false: two distinct pointers fmt.Println(a.Error() == b.Error()) // true: same text, and irrelevant…What are the conventions for the text of a Go error, and how do you add context to an error before passing it up?
How do you create a custom error type, and when is a type better than
errors.Neworfmt.Errorf?type ValidationError struct { Field string Reason string} func (e *ValidationError) Error() string {…How do
panicandrecoverwork, and where mustrecoverbe called for it to catch a panic?func SafeDo(job Job) (err error) { defer func() { if r := recover(); r != nil { // called directly by the deferred func: works err = fmt.Errorf("job %s panicked: %v\n%s", job.ID, r, debug.Stack()) } }()…What is the difference between
panic,os.Exitandlog.Fatal, and what happens to deferred calls in each case?func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, "report:", err) os.Exit(1) // every defer inside run has already finished }}…When is it acceptable to
panicinstead of returning an error, and what is theMustconvention?What is the difference between
fmt.Errorfwith%wand with%v, and when would you deliberately use%v?func main() { lost := fmt.Errorf("query user: %v", sql.ErrNoRows) kept := fmt.Errorf("query user: %w", sql.ErrNoRows) fmt.Println(lost.Error() == kept.Error()) // true: same text fmt.Println(errors.Is(lost, sql.ErrNoRows)) // false: chain cut…What does this program print, and where does
errors.Isstop walking the chain?var ErrNotFound = errors.New("not found") func main() { err := fmt.Errorf("get order 7: %w", ErrNotFound) fmt.Println(err == ErrNotFound) fmt.Println(errors.Is(err, ErrNotFound)) cut := fmt.Errorf("handler: %v", err) fmt.Println(errors.Is(cut, ErrNotFound)) _, openErr := os.Open("/no/such/file") wrapped := fmt.Errorf("load config: %w", openErr) fmt.Println(os.IsNotExist(wrapped), errors.Is(wrapped, fs.ErrNotExist))}How do you get a typed error such as
*ValidationErrorout of a wrapped chain, and why does a type assertion stop working once context is added?type ValidationError struct{ Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func main() { var err error = &ValidationError{Field: "email"}…How do you make your own error type work with
errors.Isanderrors.As: what do theUnwrapandIsmethods do?type NotFound struct { Key string cause error} func (e *NotFound) Error() string { return "not found: " + e.Key }…How do you combine several errors into one, and does
errors.Isstill find each of them?var ( ErrMissingField = errors.New("missing field") ErrBadFormat = errors.New("bad format")) func Validate(c Config) error {…This function compiles and passes its tests. What is wrong with its error handling?
func SaveReport(path string, r Report) error { f, err := os.Create(path) if err != nil { log.Printf("create report: %v", err) return err } defer f.Close() data, _ := json.Marshal(r) if _, err := f.Write(data); err != nil { return errors.New("Failed to write report: " + err.Error()) } return nil}Find the bug:
Transfermoves the money, yet every call returns an error and clients retry it.func (s *Store) Transfer(ctx context.Context, from, to int64, amount int) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("transfer: begin: %w", err) } defer tx.Rollback() if err := debit(ctx, tx, from, amount); err != nil { return fmt.Errorf("transfer: debit %d: %w", from, err) } if err := credit(ctx, tx, to, amount); err != nil { return fmt.Errorf("transfer: credit %d: %w", to, err) } return fmt.Errorf("transfer: commit: %w", tx.Commit())}What happens when a goroutine panics, can another goroutine recover it, and how do you protect goroutines you start?
// Go runs fn in a new goroutine and turns a panic into a logged error.func Go(ctx context.Context, name string, fn func(context.Context)) { go func() { defer func() { if r := recover(); r != nil { slog.ErrorContext(ctx, "goroutine panicked",…Find the bug: when a job panics, the worker survives, but the job is counted as successful.
// runJob runs one job; a panic inside it must come back as an error.func runJob(job Job) error { var err error defer func() { if r := recover(); r != nil { err = fmt.Errorf("job %s panicked: %v", job.ID, r) } }() err = job.Run() return err}Do Go errors carry a stack trace? How do you get one when you need it, and why doesn't the standard library add one?
type stackError struct { err error pcs []uintptr} func (e *stackError) Error() string { return e.err.Error() }…How do you map domain errors to HTTP or gRPC status codes without leaking internals, and where should that mapping live?
// httpError translates a domain error into a status and a client-safe message.func httpError(err error) (int, string) { var ve *ValidationError switch { case errors.As(err, &ve): return http.StatusBadRequest, "invalid " + ve.Field…How would you implement retry with exponential backoff and a maximum number of attempts that stops as soon as the context is cancelled?
func Retry(ctx context.Context, attempts int, base time.Duration, op func(context.Context) error) error { var err error delay := base for i := range attempts { if err = op(ctx); err == nil || !retryable(err) { return err…How do you design the error API of a public package (sentinel values, exported types or behaviour interfaces), and what does each commit you to?
// Only the behaviour is promised; notFoundError stays private and free to change.type notFoundError struct { Resource, Key string} func (e *notFoundError) Error() string { return e.Resource + " " + e.Key + " not found" }…Besides calling
recover, what must an HTTP recovery middleware get right (thinkhttp.ErrAbortHandlerand a response that is already half written)?type recorder struct { http.ResponseWriter wrote bool} func (r *recorder) WriteHeader(code int) { r.wrote = true; r.ResponseWriter.WriteHeader(code) }…