Error Handling
Go treats errors as ordinary values. A function that can fail returns an error as its last return value. The caller checks the error and decides what to do — there are no exceptions.
The error Interface
error is a built-in interface with a single method:
type error interface {
Error() string
}The standard way to return and check an error:
package main
import (
"errors"
"fmt"
)
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("divide by zero")
}
return a / b, nil
}
func main() {
result, err := divide(10, 3)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Printf("%.4f\n", result)
_, err = divide(5, 0)
if err != nil {
fmt.Println("error:", err) // error: divide by zero
}
}Sentinel Errors
A sentinel error is a package-level variable used to signal a specific known condition:
package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type Store struct {
data map[string]string
}
func (s Store) Get(key string) (string, error) {
v, ok := s.data[key]
if !ok {
return "", ErrNotFound
}
return v, nil
}
func main() {
store := Store{data: map[string]string{"a": "apple"}}
v, err := store.Get("a")
if err != nil {
fmt.Println(err)
} else {
fmt.Println(v) // apple
}
_, err = store.Get("z")
if errors.Is(err, ErrNotFound) {
fmt.Println("key not found — handle gracefully")
}
}Idiomatic Go: Export sentinel errors as
var Err...and check them witherrors.Is, not==. Wrapping preserves the chain, anderrors.Istraverses it.
Custom Error Types
Implement the error interface to attach structured data to an error:
package main
import (
"errors"
"fmt"
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %q: %s", e.Field, e.Message)
}
func validateAge(age int) error {
if age < 0 {
return &ValidationError{Field: "age", Message: "must be non-negative"}
}
if age > 150 {
return &ValidationError{Field: "age", Message: "unrealistically large"}
}
return nil
}
func main() {
err := validateAge(-5)
var ve *ValidationError
if errors.As(err, &ve) {
fmt.Printf("Field: %s\nMessage: %s\n", ve.Field, ve.Message)
}
}Wrapping Errors with %w
Use fmt.Errorf with %w to wrap an error, preserving its identity while adding context:
package main
import (
"errors"
"fmt"
)
var ErrDatabase = errors.New("database error")
func fetchUser(id int) error {
if id <= 0 {
return fmt.Errorf("fetchUser: %w", ErrDatabase)
}
return nil
}
func getProfile(userID int) error {
if err := fetchUser(userID); err != nil {
return fmt.Errorf("getProfile: %w", err)
}
return nil
}
func main() {
err := getProfile(-1)
fmt.Println(err) // getProfile: fetchUser: database error
fmt.Println(errors.Is(err, ErrDatabase)) // true — chain traversal
}errors.Is vs errors.As
errors.Is(err, target)— checks if any error in the chain equalstarget.errors.As(err, &target)— finds the first error in the chain that can be assigned totarget.
package main
import (
"errors"
"fmt"
)
type NotFoundError struct{ Name string }
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%q not found", e.Name)
}
func main() {
base := &NotFoundError{Name: "config.yaml"}
wrapped := fmt.Errorf("load failed: %w", base)
fmt.Println(errors.Is(wrapped, base)) // true
var nfe *NotFoundError
if errors.As(wrapped, &nfe) {
fmt.Println("missing:", nfe.Name) // missing: config.yaml
}
}Key Takeaways
- Return
erroras the last value; check it withif err != nil. - Use
errors.Newfor simple messages; implement theerrorinterface for structured errors. - Wrap errors with
fmt.Errorf("context: %w", err)to add context without losing identity. - Use
errors.Isto check for sentinel errors anderrors.Asto extract typed errors from a chain. - Avoid ignoring errors — Go makes this explicit by design.