Errors as Values
Overview
Go treats errors as values, not exceptions. Functions return errors alongside results, making error handling explicit and visible.
The error Interface
type error interface {
Error() string
}Returning Errors
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
result, err := divide(10, 0)
if err != nil {
log.Fatal(err)
}Creating Errors
import "errors"
// Simple error
err := errors.New("something went wrong")
// Formatted error
err := fmt.Errorf("failed to open %s: %v", filename, originalErr)Error Handling Patterns
Check Immediately
result, err := doSomething()
if err != nil {
return err
}
// Use resultEarly Return
func process() error {
data, err := fetch()
if err != nil {
return err
}
result, err := transform(data)
if err != nil {
return err
}
return save(result)
}Add Context
data, err := readFile(path)
if err != nil {
return fmt.Errorf("loading config: %w", err)
}Custom Error Types
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func validate(u User) error {
if u.Name == "" {
return &ValidationError{Field: "name", Message: "required"}
}
return nil
}Sentinel Errors
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func find(id int) (*User, error) {
if !exists(id) {
return nil, ErrNotFound
}
// ...
}
// Check
if err == ErrNotFound {
// Handle not found
}Summary
| Pattern | Usage |
|---|---|
errors.New() |
Simple error |
fmt.Errorf() |
Formatted error |
| Custom type | Structured error data |
| Sentinel | Known error values |
Worked example
Multi-step pipeline that returns, wraps, and classifies errors without panicking.
Save as main.go. Then:
go mod init example
go run .package main
import (
"errors"
"fmt"
)
var ErrEmpty = errors.New("empty input")
type ParseError struct {
Token string
Why string
}
func (e *ParseError) Error() string {
return fmt.Sprintf("parse %q: %s", e.Token, e.Why)
}
func parseIntToken(s string) (int, error) {
if s == "" {
return 0, ErrEmpty
}
n := 0
for _, r := range s {
if r < '0' || r > '9' {
return 0, &ParseError{Token: s, Why: "not digits"}
}
n = n*10 + int(r-'0')
}
return n, nil
}
func sumTokens(tokens []string) (int, error) {
total := 0
for i, t := range tokens {
n, err := parseIntToken(t)
if err != nil {
return 0, fmt.Errorf("sumTokens[%d]: %w", i, err)
}
total += n
}
return total, nil
}
func main() {
got, err := sumTokens([]string{"10", "20", "12"})
fmt.Println("ok sum:", got, err)
_, err = sumTokens([]string{"10", "", "3"})
fmt.Println("empty err:", err)
fmt.Println("Is ErrEmpty:", errors.Is(err, ErrEmpty))
_, err = sumTokens([]string{"10", "x9"})
var pe *ParseError
if errors.As(err, &pe) {
fmt.Println("As ParseError token:", pe.Token, "why:", pe.Why)
}
}Expected output:
ok sum: 42 <nil>
empty err: sumTokens[1]: empty input
Is ErrEmpty: true
As ParseError token: x9 why: not digits
More examples
Sentinel comparison vs string equality—only the value works reliably after wrapping.
package main
import (
"errors"
"fmt"
)
var ErrDenied = errors.New("denied")
func authorize(role string) error {
if role != "admin" {
return fmt.Errorf("authorize(%s): %w", role, ErrDenied)
}
return nil
}
func main() {
err := authorize("guest")
// Correct: compare the sentinel through the chain.
fmt.Println("Is denied:", errors.Is(err, ErrDenied))
// Fragile: string form changes when you add context.
fmt.Println("string equal ErrDenied:", err.Error() == ErrDenied.Error())
fmt.Println("err text:", err)
}Expected output:
Is denied: true
string equal ErrDenied: false
err text: authorize(guest): denied
Runnable example
Save as main.go. Then:
go mod init example
go run .package main
import (
"errors"
"fmt"
)
var ErrNotFound = errors.New("not found")
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func findUser(id int) (string, error) {
if id <= 0 {
return "", ErrNotFound
}
return fmt.Sprintf("user-%d", id), nil
}
func validateName(name string) error {
if name == "" {
return &ValidationError{Field: "name", Message: "required"}
}
return nil
}
func main() {
q, err := divide(10, 2)
fmt.Println("divide ok:", q, err)
_, err = divide(1, 0)
fmt.Println("divide err:", err)
name, err := findUser(7)
fmt.Println("find:", name, err)
_, err = findUser(0)
if errors.Is(err, ErrNotFound) {
fmt.Println("sentinel: not found")
}
if err := validateName(""); err != nil {
fmt.Println("validation:", err)
}
if err := validateName("Ada"); err != nil {
fmt.Println("unexpected:", err)
} else {
fmt.Println("validation: ok")
}
}Expected output:
divide ok: 5 <nil>
divide err: division by zero
find: user-7 <nil>
sentinel: not found
validation: name: required
validation: ok
What to notice: Errors are ordinary values you return and inspect. Sentinel errors (ErrNotFound) and custom types (ValidationError) both implement the same error interface.
Try next: Use fmt.Errorf("findUser(%d): %w", id, ErrNotFound) and compare string form vs errors.Is; add a second field to ValidationError for a code.