Kodewick
Start learning →
Lesson 14 of 24Contents

Errors

Lesson 14 of 24 · Part 4 · Errors and functions · about 20 minutes

Back in lesson 9, strconv.Atoi turned "42" into 42, and we threw its second result away with _. That second result is an error: it says whether the conversion worked. Now let's keep it:

prog.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	n, err := strconv.Atoi("42")10	fmt.Println(n, err)11	n, err = strconv.Atoi("4x2")12	fmt.Println(n, err)13}
Output
42 <nil>
0 strconv.Atoi: parsing "4x2": invalid syntax

When it works, err is nil, which fmt shows as <nil>: nothing went wrong. When it doesn't, n is 0 and err describes the problem: strconv.Atoi: parsing "4x2": invalid syntax.

if err != nil

Go has no exceptions that jump out of a function on their own (panic is the exception, lesson 15). A function that can fail returns an error as its last result, and the caller checks it right away. error is an interface with a single method:

type error interface { Error() string }

A nil error means "no error". Anything else is a problem, and fmt shows it by calling Error(). The check you'll write over and over is if err != nil:

prog.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	inputs := []string{"120", "35", "abc", "8"}10	total := 011	for _, s := range inputs {12		n, err := strconv.Atoi(s)13		if err != nil {14			fmt.Println("Skipping:", err)15			continue16		}17		total += n18	}19	fmt.Println("Total:", total)20}
Output
Skipping: strconv.Atoi: parsing "abc": invalid syntax
Total: 163

The bad input is reported and skipped; the rest still add up to 163. The usual shape: handle the error first, inside the if, and keep the normal path unindented below it.

Your own errors: errors.New

Your functions can return errors too. errors.New makes one from a message. Return the error last, and nil when all went well:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8func withdraw(balance, amount int) (int, error) {9	if amount <= 0 {10		return balance, errors.New("amount must be positive")11	}12	if amount > balance {13		return balance, errors.New("not enough money")14	}15	return balance - amount, nil16}17 18func main() {19	balance := 10020	for _, amount := range []int{30, 200, -5, 50} {21		newBalance, err := withdraw(balance, amount)22		if err != nil {23			fmt.Println("Withdraw", amount, "failed:", err)24			continue25		}26		balance = newBalance27		fmt.Println("Withdrew", amount, "balance", balance)28	}29}
Output
Withdrew 30 balance 70
Withdraw 200 failed: not enough money
Withdraw -5 failed: amount must be positive
Withdrew 50 balance 20

By habit, Go error messages start with a lowercase letter and have no period at the end, because they often end up in the middle of a longer message.

Guess first

What does this show?

1package main2 3import (4	"errors"5	"fmt"6)7 8func half(n int) (int, error) {9	if n%2 != 0 {10		return 0, errors.New("odd number")11	}12	return n / 2, nil13}14 15func main() {16	a, err := half(10)17	fmt.Println(a, err)18	b, err := half(7)19	fmt.Println(b, err)20}

Errors with details: fmt.Errorf

"not enough money" doesn't say how much was asked for. fmt.Errorf works like fmt.Sprintf, but makes an error:

prog.go
1package main2 3import "fmt"4 5func withdraw(balance, amount int) (int, error) {6	if amount > balance {7		return balance, fmt.Errorf("withdraw %d: balance is only %d", amount, balance)8	}9	return balance - amount, nil10}11 12func main() {13	_, err := withdraw(80, 120)14	fmt.Println(err)15	left, err := withdraw(80, 30)16	fmt.Println(left, err)17}
Output
withdraw 120: balance is only 80
50 <nil>

Wrapping with %w and errors.Is

Often a program needs to know which error happened, not just its message. A common way: make the error once, in a variable outside any function (the whole program can use it), and compare against it. Here a map plays the part of a folder of files:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrNotFound = errors.New("not found")9 10var files = map[string]string{11	"notes.txt": "buy milk",12	"todo.txt":  "call the bank",13}14 15func readFile(name string) (string, error) {16	text, ok := files[name]17	if !ok {18		return "", fmt.Errorf("read %s: %w", name, ErrNotFound)19	}20	return text, nil21}22 23func main() {24	for _, name := range []string{"notes.txt", "photo.jpg"} {25		text, err := readFile(name)26		if errors.Is(err, ErrNotFound) {27			fmt.Println("No such file:", err)28			continue29		}30		fmt.Println(name, "says:", text)31	}32}
Output
notes.txt says: buy milk
No such file: read photo.jpg: not found

%w in fmt.Errorf wraps an error: the new error's message includes ErrNotFound's message, and it keeps ErrNotFound inside. errors.Is(err, ErrNotFound) looks through every layer for it. A plain == only compares the outer layer:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrNotFound = errors.New("not found")9 10func main() {11	err := fmt.Errorf("read photo.jpg: %w", ErrNotFound)12	fmt.Println(err == ErrNotFound)13	fmt.Println(errors.Is(err, ErrNotFound))14	fmt.Println(errors.Unwrap(err) == ErrNotFound)15}
Output
false
true
true

errors.Unwrap takes off one layer. With %v instead of %w the message would look the same, but nothing would be kept inside, and errors.Is would say false.

Your own error type

Any type with an Error() string method is an error. A struct can carry the details as fields, so the caller can use the numbers, not just read a sentence:

prog.go
1package main2 3import (4	"errors"5	"fmt"6)7 8type BalanceError struct {9	Amount, Balance int10}11 12func (e *BalanceError) Error() string {13	return fmt.Sprintf("withdraw %d: short by %d", e.Amount, e.Amount-e.Balance)14}15 16func withdraw(balance, amount int) (int, error) {17	if amount > balance {18		return balance, &BalanceError{Amount: amount, Balance: balance}19	}20	return balance - amount, nil21}22 23func main() {24	_, err := withdraw(50, 80)25	err = fmt.Errorf("pay rent: %w", err)26	fmt.Println(err)27	var be *BalanceError28	if errors.As(err, &be) {29		fmt.Println("Missing:", be.Amount-be.Balance)30	}31}
Output
pay rent: withdraw 80: short by 30
Missing: 30

errors.As is the type-assertion version of errors.Is: it looks through the layers for an error of type *BalanceError and, if it finds one, puts it in be. That's why it gets &be, a pointer to the variable it should fill in (lesson 11).

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

An average that can fail

Finish average: return the average of nums and nil, but if nums is empty, return 0 and an error with the message no numbers. The program tries two slices.

It should print
Average: 7.5
Error: no numbers
exercise1.go
1package main2 3import (4	"errors"5	"fmt"6)7 8func average(nums []int) (float64, error) {9	return 0, nil10}11 12func main() {13	for _, nums := range [][]int{{6, 9, 7, 8}, {}} {14		avg, err := average(nums)15		if err != nil {16			fmt.Println("Error:", err)17			continue18		}19		fmt.Println("Average:", avg)20	}21}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: Atoi gives back two things

This program doesn't compile. Make it check each age: show Age: 30 for a number and Not a number: thirty when strconv.Atoi fails.

It should print
Age: 30
Not a number: thirty
exercise2.go
1package main2 3import (4	"fmt"5	"strconv"6)7 8func main() {9	for _, s := range []string{"30", "thirty"} {10		n := strconv.Atoi(s)11		fmt.Println("Age:", n)12	}13}
Output
Press Run or Ctrl+Enter
Exercise 3

Out of stock, wrapped

Finish buy: if stock[item] is less than qty, return an error that wraps ErrOutOfStock with the message buy 3 ink: out of stock (use the real item and quantity); otherwise return nil.

It should print
OK: pen
Sorry: buy 3 ink: out of stock
exercise3.go
1package main2 3import (4	"errors"5	"fmt"6)7 8var ErrOutOfStock = errors.New("out of stock")9 10var stock = map[string]int{"pen": 5, "ink": 1}11 12func buy(item string, qty int) error {13	return nil14}15 16func main() {17	for _, item := range []string{"pen", "ink"} {18		err := buy(item, 3)19		if errors.Is(err, ErrOutOfStock) {20			fmt.Println("Sorry:", err)21		} else {22			fmt.Println("OK:", item)23		}24	}25}
Output
Press Run or Ctrl+Enter

What you learned

  • A function that can fail returns an error as its last result; nil means it worked.
  • error is an interface: Error() string. Check with if err != nil right after the call.
  • strconv.Atoi returns the number and an error; on bad input the number is 0.
  • errors.New("…") makes a simple error; fmt.Errorf adds details like Sprintf.
  • %w wraps an error inside another; errors.Is finds it through every layer, errors.Unwrap takes off one.
  • Your own type with an Error() method is an error; errors.As(err, &target) finds one and fills in target.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.