Creating Errors
errors.New ยท fmt.ErrorfGo errors are just values that implement the
error interface: Error() string. Use errors.New for static messages and fmt.Errorf when you need to embed dynamic values.
errors.New โ simple static errors
errors.New
import "errors" // Define sentinel errors at package level var ( ErrNotFound = errors.New("not found") ErrPermission = errors.New("permission denied") ErrTimeout = errors.New("operation timed out") ) func findUser(id int) (*User, error) { if id <= 0 { return nil, ErrNotFound } // ... }
fmt.Errorf โ errors with context
fmt.Errorf
import "fmt" func loadConfig(path string) error { _, err := os.Open(path) if err != nil { // %w wraps the original error return fmt.Errorf("loadConfig %q: %w", path, err) } return nil } // Without %w โ context only, no unwrapping return fmt.Errorf("invalid port %d", port)
Wrapping & Unwrapping
%w ยท errors.UnwrapWrap errors as they travel up the call stack to add context at each layer. The convention is
"operationName context: %w" โ a colon-separated chain of what went wrong and where.
Building an error chain with %w
Wrapping
// Low-level: returns a raw OS error func readFile(path string) ([]byte, error) { data, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("readFile: %w", err) } return data, nil } // Mid-level: adds more context func loadConfig(env string) (*Config, error) { data, err := readFile("/etc/app/" + env + ".json") if err != nil { return nil, fmt.Errorf("loadConfig %q: %w", env, err) } // ... } // Resulting error message: // loadConfig "prod": readFile: open /etc/app/prod.json: no such file or directory
errors.Unwrap โ peel one layer off
Unwrap
var ErrBase = errors.New("base error") wrapped := fmt.Errorf("layer 1: %w", ErrBase) wrapped2 := fmt.Errorf("layer 2: %w", wrapped) errors.Unwrap(wrapped2) // layer 1: base error errors.Unwrap(wrapped) // base error (ErrBase) errors.Unwrap(ErrBase) // nil โ nothing left to unwrap // In practice you rarely call Unwrap directly. // Use errors.Is / errors.As instead โ they traverse the chain for you.
errors.Is
Sentinel value matchingerrors.Is(err, target) walks the entire error chain (unwrapping at each step) and returns true if any error in the chain equals target. Never use == for wrapped errors.
errors.Is โ check for a specific sentinel error
errors.Is
import ( "database/sql" "errors" ) func getUser(id int) (*User, error) { row, err := db.QueryRow("SELECT ...", id) if err != nil { return nil, fmt.Errorf("getUser %d: %w", id, err) } return row, nil } user, err := getUser(99) if errors.Is(err, sql.ErrNoRows) { // Correctly matches even though err is wrapped fmt.Println("user not found") } // DO NOT use == for wrapped errors if err == sql.ErrNoRows { // WRONG โ misses wrapped errors }
Custom Is method โ value-based matching
Custom Is
// Implement Is on your type when equality needs custom logic type HTTPError struct { Code int Message string } func (e *HTTPError) Error() string { return fmt.Sprintf("HTTP %d: %s", e.Code, e.Message) } // Is matches on Code alone, ignoring Message func (e *HTTPError) Is(target error) bool { t, ok := target.(*HTTPError) return ok && e.Code == t.Code } ErrNotFound := &HTTPError{Code: 404} err := fmt.Errorf("request failed: %w", &HTTPError{Code: 404, Message: "page gone"}) errors.Is(err, ErrNotFound) // true โ Code 404 matches
errors.As
Type-based extractionerrors.As(err, &target) walks the chain and assigns the first error that can be assigned to target's type. Use it when you need to read fields off a specific error type.
errors.As โ extract a typed error from the chain
errors.As
type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation: %s %s", e.Field, e.Message) } func validateAge(age int) error { if age < 0 { return &ValidationError{Field: "age", Message: "must be non-negative"} } return nil } err := fmt.Errorf("createUser: %w", validateAge(-1)) var ve *ValidationError if errors.As(err, &ve) { // ve is now populated with the ValidationError fmt.Printf("bad field: %s โ %s\n", ve.Field, ve.Message) // bad field: age โ must be non-negative }
Custom As method โ control how As matches your type
Custom As
// Implement As when your error wraps another and you need // errors.As to "see through" into a nested error field type DBError struct { Query string Cause error } func (e *DBError) Error() string { return e.Cause.Error() } func (e *DBError) Unwrap() error { return e.Cause } // errors.As walks Unwrap() automatically โ no custom As needed here. // Only implement As when Unwrap isn't sufficient.
Custom Error Types
error interface ยท UnwrapAny type that implements
Error() string satisfies the error interface. Add an Unwrap() error method to make your type work seamlessly with errors.Is and errors.As.
Structured error type with Unwrap
Custom Type
type RequestError struct { StatusCode int URL string Err error } func (e *RequestError) Error() string { return fmt.Sprintf("%s โ HTTP %d: %v", e.URL, e.StatusCode, e.Err) } // Unwrap lets errors.Is and errors.As look inside func (e *RequestError) Unwrap() error { return e.Err } // Usage err := &RequestError{ StatusCode: 503, URL: "https://api.example.com/data", Err: ErrServiceUnavailable, } var re *RequestError if errors.As(err, &re) { fmt.Printf("failed URL: %s (status %d)\n", re.URL, re.StatusCode) }
Sentinel errors vs. typed errors
Pattern
// Sentinel: simple, check with errors.Is var ErrNotFound = errors.New("not found") // Typed: carries data, check with errors.As type NotFoundError struct{ Resource string; ID int } func (e *NotFoundError) Error() string { return fmt.Sprintf("%s %d not found", e.Resource, e.ID) } // Caller pattern var nfe *NotFoundError switch { case errors.As(err, &nfe): fmt.Printf("missing %s #%d\n", nfe.Resource, nfe.ID) case errors.Is(err, ErrNotFound): fmt.Println("something not found") default: fmt.Println("unexpected error:", err) }
errors.Join
Multiple errors ยท Go 1.20+errors.Join (Go 1.20) combines multiple errors into one. The result's Unwrap returns a slice, so errors.Is and errors.As check all of them.
errors.Join โ accumulate multiple errors
Go 1.20+
func validateUser(u User) error { var errs []error if u.Name == "" { errs = append(errs, errors.New("name is required")) } if u.Age < 0 { errs = append(errs, errors.New("age must be non-negative")) } if u.Email == "" { errs = append(errs, errors.New("email is required")) } return errors.Join(errs...) // nil if errs is empty } err := validateUser(User{Age: -1}) // err.Error(): // name is required // age must be non-negative // email is required // errors.Is still works across the joined set errors.Is(err, ErrSomeSentinel) // checks all joined errors
Patterns & Best Practices
Idioms ยท dos and don'ts
Idiomatic error handling flow
Pattern
// Return errors immediately โ keep the happy path at the left margin func process(id int) (string, error) { user, err := getUser(id) if err != nil { return "", fmt.Errorf("process: %w", err) } data, err := fetchData(user.Token) if err != nil { return "", fmt.Errorf("process fetchData: %w", err) } return transform(data), nil } // Handle errors at the right level โ wrap when passing up, // handle (log/respond) when you can do something about it result, err := process(42) if err != nil { var nfe *NotFoundError if errors.As(err, &nfe) { http.Error(w, nfe.Error(), http.StatusNotFound) return } http.Error(w, "internal error", http.StatusInternalServerError) log.Printf("process: %v", err) return }
Do: wrap with context
Do
// Add what, where, and relevant IDs return fmt.Errorf("getOrder %d: %w", id, err) // One wrap per function boundary is enough func Save(u *User) error { if err := db.Insert(u); err != nil { return fmt.Errorf("Save: %w", err) } return nil }
Don't: log and return
Don't
// Never log AND return โ caller logs too โ duplicate noise if err != nil { log.Printf("error: %v", err) // BAD return err } // Never swallow errors silently result, _ := riskyOp() // BAD โ ignoring errors // Either handle it or return it, not both
Quick Reference
All key functions| Function / Method | Returns | Description |
|---|---|---|
| errors.New(text) | error | Create a simple error with a static message |
| fmt.Errorf(format, ...) | error | Create error with formatted message; use %w to wrap |
| errors.Is(err, target) | bool | Walk chain; true if any error equals target |
| errors.As(err, &target) | bool | Walk chain; assign first matching typed error |
| errors.Unwrap(err) | error | Return the directly wrapped error, or nil |
| errors.Join(errs...) | error | Combine multiple errors; nil if all nil (Go 1.20+) |
| Error() string | string | Interface method โ implement to create custom errors |
| Unwrap() error | error | Implement on custom types to enable chain traversal |
| Is(error) bool | bool | Implement for custom equality logic in errors.Is |
| As(any) bool | bool | Implement for custom matching logic in errors.As |