Irbisa · Cheatsheet

Errors & Panics

error values, wrapping with %w, errors.Is/As, panic and recover

Junior Developer · 21 questions

  1. 01

    How does error handling work in Go: what is the error type, and why does Go return errors instead of throwing exceptions?

    A function that can fail returns an error as its last result, and the caller checks if err != nil right there; error is a built-in interface with a single method, Error() string.

    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 {…
  2. 02

    What is a sentinel error, how do you declare and compare one, and why is errors.New("x") == errors.New("x") false?

    A sentinel is a package-level error variable such as io.EOF or sql.ErrNoRows that callers compare against.

    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…
  3. 03

    What are the conventions for the text of a Go error, and how do you add context to an error before passing it up?

    An error string is a fragment, not a sentence: lowercase, no full stop, no failed to.

  4. 04

    How do you create a custom error type, and when is a type better than errors.New or fmt.Errorf?

    Any type with an Error() string method is an error.

    type ValidationError struct {	Field  string	Reason string} func (e *ValidationError) Error() string {…
  5. 05

    How do panic and recover work, and where must recover be called for it to catch a panic?

    panic stops normal execution and unwinds the current goroutine, running its deferred calls on the way.

    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())		}	}()…
  6. 06

    What is the difference between panic, os.Exit and log.Fatal, and what happens to deferred calls in each case?

    panic unwinds the goroutine's stack and runs its deferred calls, so recover can stop it; os.Exit ends the process on the spot and runs no defers; log.Fatal is a log line followed by os.Exit(1).

    func main() {	if err := run(); err != nil {		fmt.Fprintln(os.Stderr, "report:", err)		os.Exit(1) // every defer inside run has already finished	}}…
  7. 07

    When is it acceptable to panic instead of returning an error, and what is the Must convention?

    Panic for programmer errors (states that mean the program itself is wrong) and return an error for everything that can fail at run time.

  8. 08

    What is the difference between fmt.Errorf with %w and with %v, and when would you deliberately use %v?

    %w wraps: the new error keeps the original value and gets an Unwrap method, so errors.Is and errors.As still find the cause.

    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…
  9. 09

    What does this program print, and where does errors.Is stop 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))}

    It prints false, true, false, then false true.

  10. 10

    How do you get a typed error such as *ValidationError out of a wrapped chain, and why does a type assertion stop working once context is added?

    Use errors.As(err, &target): it walks the same chain as errors.Is and assigns the first error of the target's type.

    type ValidationError struct{ Field string } func (e *ValidationError) Error() string { return "invalid " + e.Field } func main() {	var err error = &ValidationError{Field: "email"}…
  11. 11

    How do you make your own error type work with errors.Is and errors.As: what do the Unwrap and Is methods do?

    Give the type an Unwrap() error method that returns its cause, and errors.Is and errors.As will walk through it.

    type NotFound struct {	Key   string	cause error} func (e *NotFound) Error() string { return "not found: " + e.Key }…
  12. 12

    How do you combine several errors into one, and does errors.Is still find each of them?

    Use errors.Join (Go 1.20+) or several %w verbs in one fmt.Errorf.

    var (	ErrMissingField = errors.New("missing field")	ErrBadFormat    = errors.New("bad format")) func Validate(c Config) error {…
  13. 13

    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}

    Four things: it logs an error and also returns it, it discards the json.Marshal error, it throws away the write error's cause by concatenating its text, and it ignores the error from Close on a file it has written to.

  14. 14

    Find the bug: Transfer moves 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())}

    fmt.Errorf returns a non-nil error even when the value passed to %w is nil, so a successful commit comes back as transfer: commit: %!w(<nil>).

  15. 15

    What happens when a goroutine panics, can another goroutine recover it, and how do you protect goroutines you start?

    An unrecovered panic in any goroutine crashes the whole process, and only a recover in that same goroutine's deferred calls can stop it; a recovery middleware or a recover in the parent never sees it.

    // 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",…
  16. 16

    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}

    The result of runJob is unnamed, so the deferred closure writes to a local err that nothing returns.

  17. 17

    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?

    No: an error from errors.New or fmt.Errorf holds only a message and its wrapped cause.

    type stackError struct {	err error	pcs []uintptr} func (e *stackError) Error() string { return e.err.Error() }…
  18. 18

    How do you map domain errors to HTTP or gRPC status codes without leaking internals, and where should that mapping live?

    In one function at the transport layer: the domain returns its own sentinels and typed errors, and the handler or interceptor translates them with errors.Is and errors.As into a status and a safe message, with 500 or codes.Internal for anything unknown.

    // 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…
  19. 19

    How would you implement retry with exponential backoff and a maximum number of attempts that stops as soon as the context is cancelled?

    Loop up to N attempts, return on success or on an error that cannot succeed later, and between attempts wait in a select on ctx.Done() and a timer whose delay doubles each time, with a cap and random jitter.

    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…
  20. 20

    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?

    Every error a caller can match with errors.Is or errors.As is part of your API, as binding as a function signature.

    // 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" }…
  21. 21

    Besides calling recover, what must an HTTP recovery middleware get right (think http.ErrAbortHandler and a response that is already half written)?

    It must re-panic http.ErrAbortHandler, which handlers use on purpose to abort a response, and it may write a 500 only if nothing has been sent yet.

    type recorder struct {	http.ResponseWriter	wrote bool} func (r *recorder) WriteHeader(code int) { r.wrote = true; r.ResponseWriter.WriteHeader(code) }…