Errors¶
Go has no exceptions. When a function can fail, it returns an extra
value of type error, and the caller checks it explicitly.
n, err := strconv.Atoi("forty-two")
if err != nil {
fmt.Println("parse failed:", err)
return
}
fmt.Println(n)
This pattern — check err, react, return — is the most common
five-line block in any Go program.
From Python: Python uses exceptions: a failure interrupts control flow and unwinds the stack until something catches it. Go makes failure an ordinary return value. The mental shift is real: every fallible operation is visible at the call site, and you handle it (or explicitly pass it along) right there.
The error type¶
error is a built-in interface with one method. (An interface,
for now, is a named set of method signatures any type can satisfy by
implementing them — full treatment in a later article.)
Anything with an Error() string method satisfies it. You don't need
to know how to define one yet — that comes after methods and
interfaces.
The zero value of error is nil, which means "no error." That's
why if err != nil { ... } is the standard check.
Creating an error: errors.New¶
errors.New returns a brand-new error carrying just a message.
import "errors"
err := errors.New("something went wrong")
fmt.Println(err) // something went wrong
fmt.Println(err.Error()) // something went wrong
Each call returns a distinct value — two errors with identical
text are not equal under ==:
If you want a comparable, package-level error to check against, declare it once and reuse it:
var ErrNotFound = errors.New("not found")
func lookup(id int) (string, error) {
if id == 0 {
return "", ErrNotFound
}
return "found", nil
}
if _, err := lookup(0); err == ErrNotFound {
fmt.Println("nothing matched")
}
These package-level errors are called sentinel errors. Convention:
name them ErrXxx.
Wrapping errors with context: fmt.Errorf + %w¶
When an error bubbles up through several layers, each layer usually
wants to add context. The %w verb in fmt.Errorf builds a
wrapped error that preserves the original underneath the new
message.
func loadConfig(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("loadConfig %q: %w", path, err)
}
_ = data
return nil
}
The string you get out reads naturally:
But more importantly, the underlying error is still inspectable
through errors.Is and errors.As.
Use %w exactly once per fmt.Errorf call. To embed an error message
without wrapping (rare), use %s or %v.
Inspecting wrapped errors: errors.Is and errors.As¶
errors.Is(err, target) bool¶
Walks the wrap chain looking for a value equal to target. Use this
to compare against a sentinel.
_, err := os.Open("missing.txt")
if errors.Is(err, fs.ErrNotExist) {
fmt.Println("file does not exist")
}
The check survives any number of fmt.Errorf("...: %w", err) wraps.
Prefer errors.Is(err, ErrFoo) over err == ErrFoo — it works the
same when nothing has wrapped, and keeps working when something does.
errors.As(err, &target) bool¶
Walks the wrap chain looking for an error of a specific concrete
type, and on success copies that error into target. Use this when
you need to read fields off the underlying error.
_, err := os.Open("missing.txt")
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
fmt.Println("failed at path:", pathErr.Path)
fmt.Println("operation was:", pathErr.Op)
}
You always pass a pointer to the target variable.
Joining multiple errors: errors.Join¶
Sometimes a function performs several independent steps and you want
to report every failure, not just the first. errors.Join bundles
multiple errors into one; errors.Is/As then traverse all of them.
err1 := errors.New("disk full")
err2 := errors.New("network down")
both := errors.Join(err1, err2)
fmt.Println(both)
// disk full
// network down
fmt.Println(errors.Is(both, err1)) // true
fmt.Println(errors.Is(both, err2)) // true
nil arguments are skipped. If all are nil, the result is nil.
The cardinal rules¶
- Check every error. A discarded
erris almost always a bug. Use_only when you have a deliberate reason and a comment. - Wrap with context. When you return an error you didn't originate, add what you know (which file, which user, which operation).
- Prefer
errors.Isto==. It works equivalently in the unwrapped case and protects you when wrapping is added later. - Don't put error checks inside structures that hide them. No
try/except-style middleware. Theif err != nil { return err }block is the idiom — repetition is fine; it makes failure paths obvious.
What error is not¶
- Not an exception. There's no implicit propagation; you return it.
- Not a sum type /
Result<T, E>. The two return values are independent — by convention, iferr != nil, ignore the other value; iferr == nil, trust it. - Not the right tool for "the program reached an impossible state."
That's what
panicis for (12-panic-and-recover.md).