Go error handling interview questions, with answers
Go has no exceptions for ordinary failures. A function that can fail returns an error as its last result, and the caller checks it, so failure handling is visible in the code rather than hidden in a separate control-flow path. Interviewers ask how that works in practice: how errors carry information, how one error wraps another, how code tests for a particular failure, and where panic and recover fit in a language that prefers error values.
The answers below cover error values, custom types, wrapping, errors.Is and errors.As, joining errors, and the mechanics of panic and recover, with code built and run on Go 1.27. Then take the free Go diagnostic — ten questions across every Go topic in the bank — to see which of these you can explain but not yet predict.
The questions, with answers
1.How does error handling work in Go?
In short: A function that can fail returns an error as its last result; the caller checks err != nil straight away and handles or returns the error.
error is a built-in interface with one method, Error() string, and a nil error means success. Functions that can fail return it as their last result, and the idiom is to check it immediately: if err != nil, handle it, often by adding context and returning it to the caller. strconv.Atoi below fails on "12a", so the check catches it before n is used. The style is verbose but explicit: every place that can fail is visible, and the error's path is ordinary control flow. Ignoring an error means assigning it to _, which reviewers and linters notice.
n, err := strconv.Atoi("12a") if err != nil { fmt.Println("not a number") return } fmt.Println(n) // not a number
2.How do you create a custom error type in Go?
In short: Define a type with an Error() string method; it then satisfies the error interface and can carry fields that describe the failure.
Any type with an Error() string method is an error. A struct type lets the error carry structured information, such as the missing key in NoKey below, which callers can inspect instead of parsing a message. For errors that need no fields, errors.New("text") creates one, and fmt.Errorf formats a message. Package-level variables created with errors.New, such as io.EOF, serve as sentinel errors that callers compare against, and a custom type can add methods, such as Temporary() bool, that callers can check for.
type NoKey struct { Key string } func (e NoKey) Error() string { return "no key " + e.Key } func find(k string) error { return NoKey{k} } func main() { err := find("id") fmt.Println(err) // no key id }
3.How do you wrap an error in Go, and what does errors.Is do?
In short: fmt.Errorf with %w wraps an error inside a new one with more context; errors.Is walks the chain of wrapped errors to find a particular error value.
Adding context as an error travels up the call stack makes it readable, as in send: closed, but a plain message would lose the original error. The %w verb in fmt.Errorf keeps it: the new error prints the formatted text and also wraps the original, which its Unwrap method returns. errors.Is(err, target) then checks err and every error it wraps, one by one, for target, so it finds the sentinel ErrClosed even after several layers of wrapping, where a direct comparison with == would only see the outermost error. %v, by contrast, copies only the text and breaks the chain.
var ErrClosed = errors.New( "closed") func send() error { return fmt.Errorf( "send: %w", ErrClosed) } func main() { err := send() fmt.Println(err) // send: closed fmt.Println(errors.Is( err, ErrClosed)) // true }
4.What does errors.As do in Go?
In short: errors.As searches an error's chain for an error of a given type and, if it finds one, stores it in the variable you pass, so its fields can be read.
errors.Is answers whether a particular error value is in the chain; errors.As answers whether an error of a particular type is, and hands it back. You pass a pointer to a variable of the type you want, and if some error in the chain has that type, errors.As assigns it and returns true. Below, the Slow error is wrapped with extra context, yet errors.As finds it and the code reads its Op field. This replaces a type assertion, err.(Slow), which only looks at the outermost error and so fails as soon as the error has been wrapped.
type Slow struct{ Op string } func (s Slow) Error() string { return s.Op + " timed out" } func main() { err := fmt.Errorf("db: %w", Slow{"query"}) var s Slow if errors.As(err, &s) { fmt.Println(s.Op) // query } }
5.What do panic and recover do in Go?
In short: panic stops the normal flow and unwinds the goroutine's stack, running deferred calls; recover, called in a deferred function, stops the unwinding and returns the panic value.
A panic, raised by the program or by the runtime for errors such as an out-of-range index, stops the current function and unwinds the goroutine's stack, running every deferred call on the way. If nothing stops it, the program crashes with the panic value and a stack trace. recover stops it, but only when called directly inside a deferred function during the unwinding: it returns the value passed to panic, and the function that deferred it then returns normally to its caller. Called anywhere else, recover returns nil and does nothing. Servers use this pattern to turn a panic in one request handler into an error response instead of a crash.
func safe(f func()) { defer func() { r := recover() fmt.Println("got", r) }() f() } func boom() { panic("bad state") } func main() { safe(boom) // got bad state }
6.What does errors.Join do in Go?
In short: errors.Join combines several errors into one, whose message lists each on its own line and which errors.Is and errors.As can search through.
Some operations produce more than one failure, such as validating several fields or closing several resources. Since Go 1.20, errors.Join(errs...) returns a single error that wraps all of them, skipping nil values, and returns nil if every argument is nil. Its Error method prints the messages separated by newlines, as below, and errors.Is and errors.As check every joined error, so a caller can still ask whether a specific failure is among them. fmt.Errorf also accepts more than one %w verb for the same purpose when the errors should share a formatted message.
e1 := errors.New("disk full") e2 := errors.New("no network") err := errors.Join(e1, e2) fmt.Println(errors.Is(err, e2)) // true fmt.Println(err) // disk full // no network
7.What happens when a goroutine panics in Go?
In short: An unrecovered panic in any goroutine crashes the whole program; recover only works in the panicking goroutine's own deferred functions.
A panic unwinds only the goroutine it happens in, and recover works only in deferred calls of that same goroutine. A recover in main cannot catch a panic raised in a goroutine that main started, so a single goroutine that panics without recovering brings the entire program down, whatever the other goroutines are doing. Code that launches goroutines it does not fully control, such as a worker pool running submitted tasks, therefore defers a recover at the top of each goroutine and converts the panic into an error or a log line, as below. The WaitGroup makes main wait until the goroutine has finished.
var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() defer func() { fmt.Println("caught:", recover()) }() panic("worker") }() wg.Wait() // caught: worker
How the diagnostic asks it
One question from the Go bank, exactly as a sitting would show it. The bank has 4 on errors & panics and 30 across Go.
What happens when this Go code runs?
defer fmt.Println("deferred") panic("boom")
- 1It prints deferred, then the program crashes with panic: boomcorrect
- 2It crashes with panic: boom and never prints deferred
- 3It prints deferred and exits normally
- 4It does not compile: panic needs an error value
A panic stops normal execution and unwinds the goroutine's stack, running every deferred call on the way, which is what makes defer reliable for unlocking mutexes and closing files. So deferred is printed. Nothing recovers the panic, so once the deferred calls have run the program crashes, printing panic: boom and the goroutine's stack trace, with exit status 2. Exiting normally would need a deferred function that calls recover. panic takes a value of any type, a string included, so it compiles.
Measure it
Reading answers tells you what’s true. A diagnostic tells you what you get wrong.
10 Go questions across its topics, easy to hard, about fifteen minutes. You get a readiness figure with the arithmetic shown, the topics you missed named, and a practice set sized for today. Free: 1 diagnostic a month and 15 problems a day. No card.