Q&A
How to handle errors in Golang
Overview
Unlike languages with try/catch, Go treats errors as values returned from functions. This makes error handling explicit and visible in the code flow - you can't accidentally ignore that a function might fail, because the error is right there in the return signature. The idiomatic pattern is the ubiquitous if err != nil check.
graph TD
A["result, err := doSomething()"] --> B{err != nil?}
B -->|yes| C[Handle: return/log/wrap]
B -->|no| D[Use result, continue]
C --> E["Wrap with fmt.Errorf(...%w...)"]
E --> F["Inspect later: errors.Is / errors.As"]
style B fill:#1e1b4b,color:#fff
The Basic Pattern
result, err := strconv.Atoi("123")
if err != nil {
// handle the error
return fmt.Errorf("failed to parse: %w", err)
}
fmt.Println(result)
error is a built-in interface:
type error interface {
Error() string
}
Anything with an Error() string method is an error.
Creating Errors
import "errors"
err := errors.New("something went wrong") // simple
err := fmt.Errorf("user %d not found", id) // formatted
err := fmt.Errorf("loading config: %w", underlying) // WRAP another error
The %w verb wraps an underlying error, preserving it for later inspection while adding context.
Inspecting Errors: errors.Is and errors.As
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) { // is this (or a wrapped) ErrNotFound?
// handle the not-found case
}
var pathErr *os.PathError
if errors.As(err, &pathErr) { // is there a *os.PathError in the chain?
fmt.Println(pathErr.Path) // access its fields
}
errors.Is- checks whether the error chain matches a sentinel error value.errors.As- extracts a specific error type from the chain.
These work through %w-wrapped chains, which is why wrapping is preferred over just reformatting the message.
Custom Error Types
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Msg)
}
// usage
return &ValidationError{Field: "email", Msg: "is required"}
Custom types let callers use errors.As to access structured fields.
Sentinel Errors
Predefined error values you can compare against:
var ErrInsufficientFunds = errors.New("insufficient funds")
// caller:
if errors.Is(err, ErrInsufficientFunds) { /* ... */ }
panic / recover (use sparingly)
panic aborts normal flow; recover (inside a deferred function) can stop a panic from crashing the program. Reserve these for unrecoverable programmer errors or to convert a panic at a package boundary - not for ordinary error handling.
func safeRun() {
defer func() {
if r := recover(); r != nil {
log.Println("recovered:", r)
}
}()
panic("boom")
}
Best Practices
- Check every error - don't discard with
_unless you genuinely don't care. - Add context when propagating:
fmt.Errorf("doing X: %w", err). - Wrap with
%wso callers can useerrors.Is/As. - Handle errors once - either handle or return, not both (avoid log-and-return duplication).
- Use sentinel errors or typed errors for cases callers must distinguish.
Why This Matters in Interviews
Go interviews focus heavily on the errors-as-values philosophy. Know the if err != nil idiom, the difference between errors.New and fmt.Errorf, what %w does (wrapping), and errors.Is vs errors.As. Crucially, articulate why Go avoids exceptions (explicitness) and why panic is not for normal errors.
Common Pitfalls
- Ignoring errors with
_or not checking them. - Using
panic/recoverfor routine error handling. - Losing the original error by reformatting instead of wrapping with
%w. - Both logging and returning the same error (double-reporting).
- Comparing errors with
==instead oferrors.Iswhen wrapping is involved.
References
- Go blog, "Working with Errors": https://go.dev/blog/go1.13-errors
- Effective Go - errors: https://go.dev/doc/effective_go#errors