Errors and Failure Design
Go has no exceptions: a function returns an error value and the caller decides what happens next. Constructing, classifying, wrapping and refusing to return one are all API design here.
part ofGo (Golang)overview, primer and where to startread it →on this pageshowhide
explore
- Values, Not Exceptions19 questions
- error as a Value3 questions
- Returning and Checking err4 questions
- Message Strings and Style4 questions
- Failures From Deferred Cleanup4 questions
- Logging Versus Returning4 questions
- Sentinels, Types and Matching16 questions
- Choosing Sentinel or Typed4 questions
- Sentinel Errors and errors.Is4 questions
- Retryable and Terminal Failures4 questions
- Typed Errors and errors.As4 questions
- Wrapping and Inspection16 questions
- Implementing Is and As4 questions
- Joined and Multiple Causes4 questions
- Context Without Leaking Detail4 questions
- Wrapping with %w4 questions
- Panic Policy and Boundaries17 questions
- Fatal Runtime Aborts5 questions
- Panicking Versus Returning4 questions
- Recovering at the Edge4 questions
- Unwinding and recover4 questions
- Standard Library Contracts13 questions
- io.EOF and Stream Endings5 questions
- os and fs Predicates4 questions
- Timeouts Versus Cancellation4 questions
questions
81 · 5 sectionsIn Go, what does `defer f.Close()` on a file you wrote to silently discard?
basics
~20 sClose returns an error and a deferred call discards it. On a file you wrote to, that error can be the only report that your data never reached the disk, so a failed write ends up looking like a success.
What does Go's built-in error interface require a type to implement?
basics
~20 serror is a predeclared interface with exactly one method, Error() string. Any type that declares that method - a struct, a named int, anything - is an error and can be returned wherever error is expected.
Why is logging an error with slog.Error and also returning it to the caller a problem?
basics
~20 sLogging and returning reports one failure twice: the function writes a log record, then whoever called it reports the same failure again. Choose one role per error - either handle it and log it, or return it and stay silent.
Why does a Go function that can fail return (result, error), and what must the caller do first?
basics
~20 sGo reports failure as an ordinary value returned last, beside the real result. The caller tests err != nil before touching the other result, because on failure that result is only a zero value, not data.
Why does Go style require errors.New strings to start lowercase and end without punctuation?
basics
~10 sAn error string is rarely printed alone. Callers wrap it, so it lands in the middle of a longer message. Lowercase text with no trailing period or newline concatenates cleanly into one readable line.
What is a sentinel error in Go, and how do you declare one so callers can detect it?
basics
~20 sA sentinel error is one package-level error value, such as var ErrNotFound = errors.New("not found"), that a package returns for a specific condition. Callers detect that condition by matching a returned error against that exported value.
How do you define a custom Go error type that carries structured fields a caller can read?
basics
~20 sDeclare a struct holding the data and give it an Error() string method, normally on a pointer receiver. Any type with that method is usable as an error, and callers read the exported fields off the concrete value.
When should a Go package export a sentinel error value rather than an exported error type?
basics
~20 sExport a sentinel value when the caller only needs a yes-or-no branch, and an error type when the caller needs data to act on. A sentinel commits the package to one identity; a type commits every exported field forever.
How should a Go package carry retryability inside the error value it returns?
basics
~20 sPut the classification in the error itself: an exported sentinel matched with errors.Is, or an error type with a method such as Retryable() bool found with errors.As. Both survive wrapping; a second bool return and message text do not.
Why should a caller use errors.Is(err, io.EOF) instead of comparing err == io.EOF?
basics
~20 sThe == operator only tests the outermost error value. If any layer in between returned a new error carrying the original as its cause, == stops matching. errors.Is walks that chain of causes and matches any error in it.
What does adding an Is(target error) bool method to your own error type change about errors.Is?
basics
~20 sAn Is method lets errors.Is treat your error as equivalent to a target it does not equal by value. While searching, errors.Is calls your Is method at your link and accepts a true answer as a match.
What does errors.Join return when some arguments are nil, and what is its Error text?
basics
~20 serrors.Join skips every nil argument and returns a plain nil if all of them are nil. Otherwise it returns one error whose message is the non-nil errors' messages joined by newlines, one per line, with no trailing newline.
In fmt.Errorf, what does the %w verb do that %v does not?
basics
~20 s%w stores the original error inside the new one, giving the result an Unwrap method so errors.Is and errors.As can still find the cause. %v only copies the error's text, so that link is gone.
How do errors.Is and errors.As walk the chain that fmt.Errorf's %w builds?
basics
~20 sBoth call Unwrap repeatedly. errors.Is compares each error in the chain to the target with ==; errors.As tries to assign each one into the pointer target's type and returns true at the first that fits.
A Go handler writes err.Error() into its 500 body and customers see the SQL and the connection string. How do you fix the error path?
basics
~10 sStop rendering err.Error() to callers. At one boundary helper, log the whole wrapped chain with a correlation id and write a fixed message plus that id. Caller-facing wording must be carried explicitly, never inherited.
How do you stop a panic from escaping a Go function and return it to the caller as an error?
basics
~20 sGive the function a named error result and defer a closure that calls recover. When recover returns non-nil, the closure assigns a wrapped error to that named result, so the caller sees an ordinary error instead of a panic.
In Go, when should a function panic instead of returning an error?
basics
~20 sPanic only for bugs the program cannot continue past: impossible internal states, broken invariants, or setup at package init. Anything a caller could reasonably hit at runtime, including bad input, missing files and I/O failure, returns an error.
What happens when two goroutines write to the same Go map without a lock?
basics
~20 sGo's built-in map is not safe for concurrent writes. The runtime notices the overlapping write and aborts the whole process with 'fatal error: concurrent map writes'. It is not a panic and the program does not continue past it.
What does Go's built-in recover() do, and where must it be called to stop a panic?
basics
~10 srecover() stops a panicking goroutine from dying and returns the value that was passed to panic. It works only when called directly inside a function that was deferred; called anywhere else it returns nil.
Where must the deferred recover live when a controller runs each reconcile in its own goroutine?
basics
~20 sInside the goroutine's own function, at the top, not in the code that started it. A recovery only takes effect in a function deferred by the goroutine that panicked, and a panic that escapes any goroutine terminates the whole process.
What does ctx.Err() return after a context is cancelled versus after its deadline passes?
basics
~10 sctx.Err() returns nil while the context is still live, context.Canceled once a cancel function has been called, and context.DeadlineExceeded once the context's deadline has passed. Match them with errors.Is, not with ==.
Why is io.EOF returned by a Read call treated as a normal end of input, not a failure?
basics
~20 sio.EOF is a sentinel error the io.Reader contract uses to say the stream is finished, so nothing went wrong. Callers detect it, stop reading and return success; they never log it or pass it on as a fault.
Given an error from os.Open, how do you check that the file does not exist?
basics
~10 sUse errors.Is(err, fs.ErrNotExist). os.Open returns a *fs.PathError wrapping the operating system's error, so comparing the returned error directly against fs.ErrNotExist is always false; errors.Is unwraps the chain and finds the sentinel.
Why can an io.Reader's Read return n > 0 bytes and io.EOF in the same call?
basics
~20 sThe io.Reader contract lets a reader that hits the end of its data while filling your buffer report both at once. Process the n bytes returned before you inspect the error, or the last chunk of the stream is silently dropped.
Why doesn't errors.Is(err, context.DeadlineExceeded) match an expired socket read deadline?
basics
~10 sThey are two unrelated sentinel values. A read that fails because an I/O deadline expired wraps os.ErrDeadlineExceeded; only a finished context.Context yields context.DeadlineExceeded. Both report Timeout() true through the net.Error interface.