Go has no exceptions. A function that can fail returns an error as its last
result, and the caller checks it. This makes the failure path explicit and
visible right where it happens.
The error interface
error is just a built-in interface with a single method. Any type with an
Error() string method is an error.
type error interface {
Error() string
}
A nil error means success; a non-nil error describes what went wrong.
Returning and checking errors
The idiomatic pattern is to return the error last and check it immediately with
if err != nil. You either handle it or return it to your own caller.
f, err := os.Open("config.txt")
if err != nil {
return err // handle it here, or propagate it
}
defer f.Close()
Creating errors with errors.New
For a simple, fixed message, use errors.New. For a message that includes
values, use fmt.Errorf.
err := errors.New("cannot divide by zero")
err = fmt.Errorf("user %d is not allowed", id)
Wrapping errors with %w
When you propagate an error, add context so the final message reads like a
trail. The %w verb in fmt.Errorf wraps the original error, keeping it
reachable for later inspection.
if err != nil {
return fmt.Errorf("load config: %w", err)
}
Sentinel errors and errors.Is
A sentinel is a predefined error value that callers compare against. Because
%w preserves the wrapped error, use errors.Is (not ==) so the match still
works through layers of wrapping.
var ErrNotFound = errors.New("not found")
if errors.Is(err, ErrNotFound) {
// handle the not-found case
}
Custom error types and errors.As
When you need to attach data to an error, define a type with an Error method.
errors.As unwraps the chain and, if it finds a matching type, fills in your
variable so you can read its fields.
type ValidationError struct {
Field string
}
func (e *ValidationError) Error() string {
return "invalid field: " + e.Field
}
var vErr *ValidationError
if errors.As(err, &vErr) {
fmt.Println("bad field:", vErr.Field)
}
A complete example
The program below looks up a user, wraps a sentinel error with context, and uses
errors.Is to react to the not-found case. It compiles and runs as-is.
package main
import (
"errors"
"fmt"
)
// ErrNotFound is a sentinel error callers can compare against.
var ErrNotFound = errors.New("user not found")
var users = map[int]string{
1: "Ada",
2: "Alan",
}
// findUser returns the name, or wraps ErrNotFound with context.
func findUser(id int) (string, error) {
name, ok := users[id]
if !ok {
return "", fmt.Errorf("findUser %d: %w", id, ErrNotFound)
}
return name, nil
}
func main() {
for _, id := range []int{1, 42} {
name, err := findUser(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
fmt.Printf("id %d: not found\n", id)
continue
}
fmt.Printf("id %d: unexpected error: %v\n", id, err)
continue
}
fmt.Printf("id %d: %s\n", id, name)
}
}
Running it prints:
id 1: Ada
id 42: not found
Cleanup with defer, and panic as a last resort
defer schedules a call to run when the surrounding function returns, whatever
path it takes. It is the standard way to release resources such as files or
locks, so cleanup sits right next to the acquisition.
f, err := os.Open("data.txt")
if err != nil {
return err
}
defer f.Close() // always runs on the way out
Common mistakes
- Ignoring errors. Writing
f, _ := os.Open(...)hides failures. Handle or return every error you get. - Wrapping with
%vinstead of%w.%vformats the message but discards the original error, soerrors.Isanderrors.Ascan no longer see it. - Comparing wrapped errors with
==. Once an error is wrapped, useerrors.Isto match a sentinel. - Panicking on ordinary failures. A missing file or bad input is expected; return an error rather than crashing the program.
Practice
Write a divide(a, b int) (int, error) that returns an error created with
errors.New when b is zero, then call it with a zero divisor and print the
error.
Show the solution
package main
import (
"errors"
"fmt"
)
func divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 0)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("result:", result)
}divide returns the error as its last value; main checks it with
if err != nil before touching result. With a zero divisor the guard fires
and the program prints error: cannot divide by zero.
Summary
erroris an interface; anilerror means success.- Return errors last and check them with
if err != nil. - Create errors with
errors.New/fmt.Errorf, and wrap with%wto keep the original reachable. - Match errors with
errors.Is(sentinels) anderrors.As(custom types).
Next, revisit the interfaces in Go that error
itself is built on. To see where errors first surface as return values, review
functions in Go.