33. Errors are values
Go has no exceptions for ordinary failure. A function that can fail returns
an error alongside its result, and the caller deals with it on the next
line. That's it — there is no invisible control flow anywhere in a Go
program.
The error type
You've already seen it: error is an interface with one method.
type error interface {
Error() string
}
Nothing more. An error is any value that can describe itself as a string,
and nil means "no error".
Creating errors
package main
import (
"errors"
"fmt"
)
func withdraw(balance, amount int) (int, error) {
if amount <= 0 {
return balance, errors.New("amount must be positive")
}
if amount > balance {
return balance, fmt.Errorf("insufficient funds: have %d, need %d", balance, amount)
}
return balance - amount, nil
}
func main() {
b, err := withdraw(100, 30)
fmt.Println(b, err)
b, err = withdraw(100, 500)
fmt.Println(b, err)
b, err = withdraw(100, -5)
fmt.Println(b, err)
}
Two ways to make one:
errors.New("...")for a fixed message.fmt.Errorf("... %d ...", x)when you want to include values. It'sSprintfthat returns anerror.
Both give you a value you can return, store, compare and pass around, which is the whole point of the design.
The shape of every Go call site
package main
import (
"fmt"
"strconv"
)
func parseAll(inputs []string) ([]int, error) {
nums := make([]int, 0, len(inputs))
for i, s := range inputs {
n, err := strconv.Atoi(s)
if err != nil {
return nil, fmt.Errorf("input %d: %w", i, err)
}
nums = append(nums, n)
}
return nums, nil
}
func main() {
nums, err := parseAll([]string{"1", "2", "3"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("parsed:", nums)
nums, err = parseAll([]string{"1", "oops", "3"})
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println("parsed:", nums)
}
if err != nil { return ..., err } is the Go idiom. People complain that
it's verbose; the trade is that failure is visible in the source, right where
it happens, instead of hidden behind a try block three frames up.
Two habits to build now:
- Check immediately. Never use a result before checking its error — on
failure it's usually a zero value or
nil, and you'll get a confusing panic instead of a clear message. - Return the zero value with the error.
return nil, errfor slices and pointers,return 0, errfor numbers. Don't return half-built results.
Handling, not just propagating
Returning the error up isn't the only option. Sometimes you handle it:
package main
import (
"fmt"
"strconv"
)
func parseOr(s string, fallback int) int {
n, err := strconv.Atoi(s)
if err != nil {
return fallback
}
return n
}
func mustParse(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic("bad literal: " + s)
}
return n
}
func main() {
fmt.Println(parseOr("42", 0))
fmt.Println(parseOr("nonsense", -1))
fmt.Println(mustParse("7"))
}
Three legitimate responses to an error: handle it (a default, a retry, a different source), return it (usually with context added), or crash (only for programmer error — see the panic lesson).
The one response that isn't legitimate is ignoring it. n, _ := strconv.Atoi(s)
compiles happily and quietly gives you 0 for garbage input.
Errors are just data
Because they're ordinary values, you can put them in a slice, count them, or collect them all instead of stopping at the first:
package main
import (
"fmt"
"strconv"
)
func parseAll(inputs []string) ([]int, []error) {
var nums []int
var errs []error
for _, s := range inputs {
n, err := strconv.Atoi(s)
if err != nil {
errs = append(errs, fmt.Errorf("%q is not a number", s))
continue
}
nums = append(nums, n)
}
return nums, errs
}
func main() {
nums, errs := parseAll([]string{"1", "two", "3", "four"})
fmt.Println("parsed:", nums)
fmt.Println("failures:", len(errs))
for _, e := range errs {
fmt.Println(" -", e)
}
}
Form validation wants every problem at once, not just the first. Try expressing that with exceptions and you'll appreciate errors-as-values.
Error message style
Go has strong conventions here, and they exist because messages get concatenated as they travel up:
- Lowercase, no trailing punctuation.
"file not found", not"File not found."— because it will end up in the middle of a longer message. - State what failed, with the specifics.
"parsing %q: %v"beats"an error occurred". - Don't start with "error:" or "failed to" — the caller adds that framing.
package main
import (
"errors"
"fmt"
)
func main() {
bad := errors.New("Error: Could not open the file!")
good := fmt.Errorf("open config.yaml: file does not exist")
fmt.Printf("reading settings: %v\n", bad)
fmt.Printf("reading settings: %v\n", good)
}
Read both output lines out loud. The second is a sentence; the first is a collision.
Your turn
Write safeDivide(a, b int) (int, error) that returns an error with the
message cannot divide by zero when b is zero. Print both cases:
5 <nil>
0 cannot divide by zero
package main
import (
"errors"
"fmt"
)
// write safeDivide here
func main() {
q, err := safeDivide(10, 2)
fmt.Println(q, err)
q, err = safeDivide(10, 0)
fmt.Println(q, err)
}
package main
import (
"errors"
"fmt"
)
func safeDivide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("cannot divide by zero")
}
return a / b, nil
}
func main() {
q, err := safeDivide(10, 2)
fmt.Println(q, err)
q, err = safeDivide(10, 0)
fmt.Println(q, err)
}
Next: how a caller tells which error it got.