Errors as Values
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.