Errors
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:
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}
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:
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}
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:
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}
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.
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}
half(10) works, so err is nil and shows as <nil>. half(7) returns 0 and the error, whose message is odd number. The second := is fine, because b is a new variable; it just assigns to the existing err.
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:
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}
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:
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}
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:
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}
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:
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}
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.
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.
Average: 7.5 Error: no numbers
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}
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.
Age: 30 Not a number: thirty
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}
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.
OK: pen Sorry: buy 3 ink: out of stock
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}
What you learned
- A function that can fail returns an
erroras its last result;nilmeans it worked. erroris an interface:Error() string. Check withif err != nilright after the call.strconv.Atoireturns the number and an error; on bad input the number is0.errors.New("…")makes a simple error;fmt.Errorfadds details likeSprintf.%wwraps an error inside another;errors.Isfinds it through every layer,errors.Unwraptakes off one.- Your own type with an
Error()method is an error;errors.As(err, &target)finds one and fills intarget.