Kodewick
Start learning →
Lesson 15 of 24Contents

defer, panic and recover

Lesson 15 of 24 · Part 4 · Errors and functions · about 15 minutes

defer puts a function call aside and runs it as the surrounding function ends. Press Step and watch the deferred line get skipped at first, then run after the last line of main:

prog.go
1package main2 3import "fmt"4 5func main() {6	fmt.Println("Open the shop")7	defer fmt.Println("Close the shop")8	fmt.Println("Serve customers")9}
Output
Open the shop
Serve customers
Close the shop

"Close the shop" is written second but shown last. You can write the cleanup right next to the setup, and it still happens at the end.

Last in, first out

Several defers in one function run in reverse order: the last one deferred runs first, like a stack of plates. Press Step to see all three wait until the loop is done:

prog.go
1package main2 3import "fmt"4 5func main() {6	for i := range 3 {7		defer fmt.Println("deferred", i)8	}9	fmt.Println("loop done")10}
Output
loop done
deferred 2
deferred 1
deferred 0

The reverse order makes sense for cleanup: what was opened last is closed first.

Arguments are evaluated right away

Guess first

What does this show?

1package main2 3import "fmt"4 5func main() {6	count := 17	defer fmt.Println("count at defer:", count)8	count = 109	fmt.Println("count now:", count)10}

To see the value at the end instead, defer a function with no name that reads the variable when it runs. func() { … }() writes a function on the spot and calls it (lesson 16 has more on these):

prog.go
1package main2 3import "fmt"4 5func main() {6	count := 17	defer func() {8		fmt.Println("count at the end:", count)9	}()10	count = 1011	fmt.Println("count now:", count)12}
Output
count now: 10
count at the end: 10

Cleanup on every return

A function with several returns would need the cleanup line before each of them. With defer you write it once, and it runs however the function ends:

prog.go
1package main2 3import "fmt"4 5func withdraw(balance, amount int) int {6	fmt.Println("Open the account")7	defer fmt.Println("Close the account")8	if amount > balance {9		fmt.Println("Not enough money")10		return balance11	}12	return balance - amount13}14 15func main() {16	fmt.Println(withdraw(100, 30))17	fmt.Println(withdraw(100, 300))18}
Output
Open the account
Close the account
70
Open the account
Not enough money
Close the account
100

Look where "Close the account" shows up: before 70. withdraw has to finish (deferred calls included) before main gets the result to show. In real programs defer closes files and unlocks locks (lesson 22).

panic

Some problems a program can't go on from: an index past the end of a slice, a nil map written to. Go panics: it stops the function, runs the deferred calls on the way out, shows a message and ends the program. You can panic yourself, too:

prog.go
1package main2 3import "fmt"4 5func setPrice(price int) int {6	if price < 0 {7		panic("price can't be negative")8	}9	return price10}11 12func main() {13	defer fmt.Println("deferred in main still runs")14	fmt.Println(setPrice(30))15	fmt.Println(setPrice(-5))16	fmt.Println("never shown")17}
Output
30
deferred in main still runs
panic: price can't be negative

goroutine 1 [running]:
main.setPrice(...)
	/tmp/sandbox/prog.go:7 +0x1d
main.main(...)
	/tmp/sandbox/prog.go:15 +0x1d
exit status 2

What happened: The program called panic (or something it called did): it stops here unless a deferred recover catches it.

First 30. Then setPrice(-5) panics: the deferred line in main still runs, and then Go shows panic: price can't be negative, the call stack (main.setPrice, called from main.main, with line numbers), and exit status 2. "never shown" really isn't shown.

recover

recover() inside a deferred function stops a panic. It returns what was passed to panic (or nil when there's no panic), and the function then returns normally. A common use is to turn a panic into an error:

prog.go
1package main2 3import "fmt"4 5func safeDivide(a, b int) (result int, err error) {6	defer func() {7		if r := recover(); r != nil {8			err = fmt.Errorf("caught: %v", r)9		}10	}()11	return a / b, nil12}13 14func main() {15	fmt.Println(safeDivide(10, 2))16	fmt.Println(safeDivide(10, 0))17	fmt.Println("still running")18}
Output
5 <nil>
0 caught: runtime error: integer divide by zero
still running

10 / 0 panics. The deferred function catches it and sets the named result err (named results, lesson 5); result stays at its zero value, 0. No panic: text, no exit status 2: the program carries on. recover only works directly inside a deferred function; called anywhere else it just returns nil.

When to panic

Hardly ever. Bad input, a missing file, not enough money: these are things that happen, and the caller should decide what to do, so return an error (lesson 14). A panic is for "this can't happen unless the program itself has a bug". Even then, prefer an error when you can.

SituationUse
A user typed abc instead of a numberreturn an error
A key isn't in the mapv, ok := m[k] or an error
A case the code should never reachpanic
A panic that mustn't stop the whole programrecover in a deferred function

Practice

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

Exercise 1

Count down with defer

Right now countdown(3) shows Ready, then 1, 2, 3. Change one line so the numbers come out backwards (3, 2, 1) after Ready.

It should print
Ready
3
2
1
Ready
2
1
exercise1.go
1package main2 3import "fmt"4 5func countdown(n int) {6	fmt.Println("Ready")7	for i := 1; i <= n; i++ {8		fmt.Println(i)9	}10}11 12func main() {13	countdown(3)14	countdown(2)15}
Output
Press Run or Ctrl+Enter
Exercise 2

Fix it: the till left open

When there isn't enough stock, sell returns early and never shows Close the till. Use defer so the till is closed on both paths.

It should print
Open the till
Close the till
Left: 3
Open the till
Only 5 left
Close the till
Left: 5
exercise2.go
1package main2 3import "fmt"4 5func sell(stock, qty int) int {6	fmt.Println("Open the till")7	if qty > stock {8		fmt.Println("Only", stock, "left")9		return stock10	}11	fmt.Println("Close the till")12	return stock - qty13}14 15func main() {16	fmt.Println("Left:", sell(5, 2))17	fmt.Println("Left:", sell(5, 9))18}
Output
Press Run or Ctrl+Enter
Exercise 3

Turn a panic into an error

safeGet(list, 5) panics: index out of range. Add a deferred function with recover that sets err to fmt.Errorf("caught: %v", r), so the program shows both lines.

It should print
30 <nil>
0 caught: runtime error: index out of range [5] with length 3
exercise3.go
1package main2 3import "fmt"4 5func safeGet(list []int, i int) (v int, err error) {6	return list[i], nil7}8 9func main() {10	prices := []int{10, 20, 30}11	fmt.Println(safeGet(prices, 2))12	fmt.Println(safeGet(prices, 5))13}
Output
Press Run or Ctrl+Enter

What you learned

  • defer f() runs f() as the surrounding function ends, however it ends.
  • Several defers run last in, first out.
  • The arguments of a deferred call are evaluated on the defer line; defer func() { … }() reads variables at the end.
  • panic stops the function, runs the deferred calls and ends the program with panic: … and exit status 2.
  • recover() inside a deferred function stops a panic; with named results it can turn it into an error.
  • Return errors for things that can happen; keep panic for bugs.
Finished this lesson?Mark it. Progress is saved in this browser, and you can undo it.