When does a defer inside a loop, a throwing function or an early-exit branch run, and when is it never registered?

27.2.2 · L2 · Interview

Predict first

What does this loop print?

func loop() {
    for i in 1...4 {
        defer { print("defer", i) }
        if i == 2 { continue }
        if i == 4 { break }
        print("body", i)
    }
    print("after loop")
}
loop()
  • body 1 / defer 1 / defer 2 / body 3 / defer 3 / defer 4 / after loop
  • body 1 / body 3 / defer 1 / defer 2 / defer 3 / defer 4 / after loop
  • body 1 / defer 1 / body 2 / defer 2 / body 3 / defer 3 / after loop

Predict first

And this one, with a throw and an early return before the defer?

struct Boom: Error {}

func thrower(_ fail: Bool) throws {
    print("enter", fail)
    defer { print("cleanup") }
    if fail { throw Boom() }
    print("no failure")
}
do { try thrower(true) } catch { print("caught") }

func registeredLate(_ go: Bool) {
    if !go { return }
    defer { print("registered only after the early return") }
    print("body")
}
registeredLate(false)
print("--")
registeredLate(true)
  • enter true / cleanup / caught / -- / body / registered only after the early return
  • enter true / caught / cleanup / -- / body / registered only after the early return
  • enter true / cleanup / caught / registered only after the early return / -- / body / registered only after the early return

Mental model. A defer is a note you put in the exit door: “when you leave this room, switch off the light”. If you never entered the room far enough to leave the note, there is nothing to run.

A defer belongs to the scope it is written in and runs whenever that scope is left: normal end, return, break, continue or a thrown error. In a loop body the scope is one iteration, so it runs every iteration, including the ones cut short by continue and the one ended by break: the output shows “defer 2” (after the continue) and “defer 4” (after the break), while “body 2” and “body 4” never print. The code after the loop runs only once the last defer has run.

In a throwing function the defer still runs on the way out, before the caller’s catch sees the error, so “cleanup” prints before “caught”. And the rule people miss: a defer is only registered when execution reaches it. In registeredLate, the early return happens first, so no defer exists and nothing prints for false; for true the body runs and then the defer runs. Place the defer immediately after the resource it cleans up, after the check that could return early, otherwise you either release something you never acquired or skip the cleanup you meant to keep.

The defer runs at the end of every iteration, including the ones left through continue and break.

The defer runs before the catch, and an early return before the defer statement means it was never registered.

Common traps

  • Putting a defer before the guard that can return early. It then runs for paths that never acquired the resource.
  • Thinking a defer in a loop runs once at the end of the function. It runs at the end of every iteration.

They might follow up with

  • In what order do several defers in the same scope run? Last written, first run (Q47).
  • Does a defer run when the process crashes? No: a trap or exit() does not unwind scopes, so do not depend on defer for crash-time cleanup.

Related questions

  • 27.2.1 A function returns x and a defer changes x afterwards. What does the caller receive, for a value, an inout, a…
  • 27.1.3 A closure uses [weak self] and the owner is released while the work is running. What is the difference…
  • 1.8.4 What does defer guarantee? What is the order of multiple defers?
  • 15.4.1 How do you model errors in Swift: throws, Result, optionals and typed throws, and how do you translate them…

← A function returns x and a defer changes x afterwards. What does the…