Go
Error Handling
Go has no exceptions at all — a deliberate design decision, not a missing feature.
Every function that can fail returns an explicit error value alongside its normal result,
using exactly the multiple-return-value
mechanism from earlier in this domain. This chapter is that convention in full, and why Go's designers
chose it over C++'s exceptions.
- What it is
- Errors as ordinary values, returned explicitly from functions and checked immediately by the caller — no throw/catch mechanism exists in the language.
- Mental model
- Where C++ says "stop everything and unwind until someone catches this," Go says "hand the error back like any other value, and let the caller decide right here, right now."
- Key numbers
erroris a one-method interface (Error() string) — the simplest possible interface in the language, and one of its most used.- Reach for it when
- Every function that can fail, essentially — checking
if err != nilimmediately after almost every call is the normal shape of idiomatic Go code.
The error interface
type error interface {
Error() string
}
That's the entire built-in definition — any type with an Error() string method satisfies
error, using the same implicit-implementation rule from
Interfaces. Creating one is usually this simple:
err := errors.New("file not found")
fmt.Println(err.Error()) // file not found
fmt.Println(err) // file not found — fmt knows to call Error() automatically
The if err != nil convention
func readConfig(path string) (string, error) {
data, err := os.ReadFile(path)
if err != nil {
return "", err // pass the error straight up to OUR caller
}
return string(data), nil // nil error means success
}
func main() {
config, err := readConfig("settings.txt")
if err != nil {
fmt.Println("failed to read config:", err)
return
}
fmt.Println(config)
}
nil is Go's zero value for an interface — an error that's nil
means "nothing went wrong." This is genuinely close to
C's own return-code convention in spirit, but Go's version is
checked by strong, near-universal convention rather than being merely legal to ignore.
Wrapping errors for context
fmt.Errorf with %w wraps an underlying error with additional context, while
keeping the original error inspectable:
func loadUser(id int) (*User, error) {
data, err := readConfig("user.txt")
if err != nil {
return nil, fmt.Errorf("loading user %d: %w", id, err) // adds context, keeps the original
}
// ...
}
// If it fails: "loading user 42: open user.txt: no such file or directory"
// errors.Is() and errors.Unwrap() can still recover the original error from the wrapped one
This solves a real problem plain string concatenation can't: the caller several layers up still has access to the original error, not just a string that mentions it, letting code check for a specific underlying failure even through several layers of added context.
panic and recover
Go does have panic, but it's reserved for genuinely unrecoverable situations — a programming
bug, not an expected failure condition:
func mustPositive(n int) int {
if n < 0 {
panic("mustPositive: negative input") // this should never happen if the code is correct
}
return n
}
The rule of thumb Go's own standard library follows: return an error for anything a caller
could reasonably expect and handle (a missing file, a bad network response); reserve panic
for violated invariants that indicate an actual bug. This is a narrower, more deliberate use than
C++ exceptions, which are commonly used for a much
wider range of error conditions.
Real-world applications
- Every standard library I/O function — file operations, network calls, and JSON parsing in Go's standard library all return
erroras their last value, without exception. - Input validation in library code — returning a descriptive
errorfrom a constructor-like function, since Go has no constructors to throw from in the first place. - Request handling in web services — each layer (database, business logic, HTTP handler) wraps errors with its own context as they propagate up, producing a readable failure trail without a stack unwind.
Gotchas
- Ignoring a returned error (
data, _ := os.ReadFile(path)) is legal Go and a genuinely common real bug — nothing forces you to checkerr, exactly the same unchecked-failure risk as an ignored C return code. - An unrecovered
paniccrashes the entire program (or, in a goroutine, can crash it even more abruptly) — usingpanicfor routine, expected error conditions is a real anti-pattern, not a shortcut. - A deferred
recover()call can catch a panic and let the program continue — but overusing this to paper overpanics used for expected failures just reinvents exception handling with extra steps, defeating the point of Go's convention. - Wrapping an error with
%wand then comparing it directly with==to the original error fails — always useerrors.Is(), which correctly unwraps the chain, when checking for a specific underlying error.