CodeGym /Courses /Swift SELF /defer — guaranteed cleanup and completion

defer — guaranteed cleanup and completion

Swift SELF
Level 31, Lesson 3
Available

1. The problem of “many exits from a function”

When you write a simple function without conditions, everything looks ideal: “did the work — printed the log at the end — exited.” But as soon as guard, if, return in the middle, and especially throw appear, the function turns into a corridor with multiple exits. And you suddenly realize that “closing the door behind you” is needed at every exit — and that is both boring and risky.

Imagine that we are writing a command handler in a console application and want to log the start and the end. In a naive version, we will place print("end") at the end... and forget that the function may exit earlier.

defer is a “sticker on the exit”: you declare the final block near the start of the work, and Swift guarantees that it will run when leaving the current scope almost in any scenario.

What is defer: registered now — executed later

The syntax of defer is very simple, and that is part of its sneaky charm: it looks like an ordinary code block { ... }, but it does not execute immediately. The main thing is not to confuse it: defer does not “start asynchronously,” does not “postpone into a queue,” and has no time magic at all. It simply says: “when we leave this scope, run this.”

The key phrase: defer runs when exiting the scope in which it is declared. This scope can be the body of a function, the body of a do { } block, the body of a loop, and even the body of an if branch if defer is declared inside that branch.

Mini-example for intuition:


import Foundation

func demoDefer() {
    print("start")
    defer { print("end") }
    print("work")
}

demoDefer()
// start
// work
// end

Here, defer will run at the moment of exiting the function demoDefer().

2. defer and function exits: return and throw

Early return: “log at the end” without copy-paste

With early return, beginners usually have the same story: “I did everything right, why is "end" not printed?” Because you exited earlier, and the code at the bottom did not run. defer solves this exactly the way you want: you write the “final part” once, and it runs even on an early exit.

Let's make a small example in the style of an input handler: if the name is empty, we exit.

import Foundation

func greet(name: String) {
    print("greet: start")
    defer { print("greet: end") }

    guard !name.isEmpty else { return }
    print("Hello, \(name)!")
}

greet(name: "")
// greet: start
// greet: end

greet(name: "Ana")
// greet: start
// Hello, Ana!
// greet: end

Notice: defer runs in both the “empty” case and the “successful” case. This is an excellent base for clean “entry/exit” logging and for resetting temporary flags.

throw: cleanup even when things go off plan

With errors, the situation is similar, but a bit more dramatic: you threw an error (throw) — and ordinary code below does not run. If you need to reliably print “finished processing” or restore a temporarily changed state, defer is the most direct way.

Let's make an example: the function requires a positive number and throws an error if the number is not suitable.

import Foundation

enum ValidationError: Error {
    case nonPositive
}

func requirePositive(_ x: Int) throws {
    print("check: start")
    defer { print("check: end") }

    guard x > 0 else { throw ValidationError.nonPositive }
    print("OK")
}

do {
    try requirePositive(0)
} catch {
    print("caught:", error) // caught: nonPositive
}

Key point: "check: end" will be printed even if throw happened. So defer is your way to guarantee “final actions” regardless of the exit scenario.

4. Order and scope: LIFO and scope

Multiple defer: LIFO order

At this point, many people think: “Okay, one defer makes sense. But what if I add two?” Swift executes them in reverse order of declaration. This is called LIFO, like a stack: the last registered defer runs first.

It sounds minor, but it helps you think correctly: if you “opened/created/turned on” something in a certain order, then “closing/removing/turning off” it usually should happen in the reverse order. And defer makes that natural.

import Foundation

func showDeferOrder() {
    print("0")
    defer { print("1") }
    defer { print("2") }
    print("3")
}

showDeferOrder()
// 0
// 3
// 2
// 1

Just remember: defer blocks run bottom-up relative to where they are declared.

defer lives in a scope, not “at the end of the function”

It is very easy to start thinking that defer means “at the end of the function.” In practice, it means “at the end of the scope.” And a scope can be not only a function, but also, for example, a separate do { } block. This is convenient when you want the cleanup to happen earlier than the function ends as a whole.

For example, you want: “inside one block, parse and print the result; when leaving the block, print "done parsing"; and then let the function continue living.”

import Foundation

func demoScopedDefer() {
    print("A")

    do {
        print("B")
        defer { print("C (end of do)") }
        print("D")
    }

    print("E")
}

demoScopedDefer()
// A
// B
// D
// C (end of do)
// E

Such a local defer helps keep the “life of temporary things” inside a small block instead of spreading it across the whole function.

5. defer for logging: “entry/exit” on any outcome

The word “logging” sounds as if we are about to build an industrial logger with levels and categories (we will get there later). But in a teaching CLI application, and at this stage of the course, an honest print is enough. And here defer is the best friend: it lets you log guaranteed symmetry.

Let's agree on a simple style: at the beginning of the function we print "[X] start", and in defer we print "[X] end". Then the logs clearly show where we entered and where we exited. This works especially nicely when there are many guards inside.

import Foundation

func parseNonEmpty(_ text: String) -> String? {
    print("[parseNonEmpty] start")
    defer { print("[parseNonEmpty] end") }

    guard !text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
        return nil
    }
    return text
}

_ = parseNonEmpty("   ")
// [parseNonEmpty] start
// [parseNonEmpty] end

Yes, it is “just print.” But the brain already feels better: functions gain a neat frame.

6. Nuances of defer: when it will not run and how values are read

defer will not run if execution never reached it

This is a subtlety because of which defer sometimes seems to “not work.” In fact, it works perfectly: defer is registered only when the interpreter reaches the line with defer. If you returned earlier, there is nothing to register.

Let's look at an example: in one variant defer is placed before guard, and in the other — after it. And these are two different programs in meaning.

import Foundation

func variantA(_ ok: Bool) {
    defer { print("A: cleanup") }
    guard ok else { return }
    print("A: work")
}

func variantB(_ ok: Bool) {
    guard ok else { return }
    defer { print("B: cleanup") }
    print("B: work")
}

variantA(false) // A: cleanup
variantB(false) // (nothing)

The practical rule is simple: if cleanup/logging is needed “on any exit,” place defer as close to the beginning as possible, right after the place where you “started work.”

defer and variable capture: the current value is printed

defer is a closure. And closures can “see” variables from the outer scope. And here it is useful to understand that usually defer will work with the current values at the time of execution, not a “snapshot at declaration time.” This is often convenient, but sometimes surprising.

Let's show this with a simple number:

import Foundation

func captureDemo() {
    var x = 1
    defer { print("x =", x) }
    x = 10
}

captureDemo() // x = 10

So defer printed 10 because by the time the function exited, x had already changed. If you need a “snapshot,” you make it manually: store a copy in a constant and use it.

import Foundation

func snapshotDemo() {
    var x = 1
    let snapshot = x
    defer { print("snapshot =", snapshot) }
    x = 10
}

snapshotDemo() // snapshot = 1

Mini table: when defer runs and when it does not

Sometimes it is easier to “see it with your eyes” once than to read text ten times. Here is a short reference table to keep in mind until defer becomes intuitive.

Scope exit scenario Will defer run? Why
Reached the end of the function Yes You exited the function scope
return from the middle Yes return is also a “scope exit”
throw Yes An error unwinds the stack, the scope is still left
break/continue in a loop Yes (for the iteration/block scope) You leave the current execution area
Exited before the defer line No defer did not have time to register

7. Example: defer in a mini app “Library in the console”

Now we will assemble a very small (but alive) piece of a “library” CLI that gradually evolves during the course: we have a list of books, and there is an "add" command that takes a title and a year. We will intentionally create several early exits to see the value of defer not in theory, but “in the logs.”

First — minimal models and errors:

import Foundation

struct Book {
    let title: String
    let year: Int
}

enum CommandError: Error {
    case emptyInput
    case unknownCommand(String)
    case missingArguments
    case invalidYear(String)
}

Now let's write the handler for the "add" command. Notice: we log the start and the end, and we do it in one line via defer.

import Foundation

func handleAdd(tokens: [String], library: inout [Book]) -> Result<Void, CommandError> {
    print("[add] start")
    defer { print("[add] end") }

    guard tokens.count >= 3 else { return .failure(.missingArguments) }
    guard let year = Int(tokens[1]) else { return .failure(.invalidYear(tokens[1])) }

    let title = tokens.dropFirst(2).joined(separator: " ")
    library.append(Book(title: title, year: year))
    return .success(())
}

Now the “run” (in a real app the input will come through readLine(), but we care more about the logic):

import Foundation

var library: [Book] = []

let r1 = handleAdd(tokens: ["add"], library: &library)
print(r1) // failure(missingArguments)

let r2 = handleAdd(tokens: ["add", "YEAR", "Dune"], library: &library)
print(r2) // failure(invalidYear("YEAR"))

let r3 = handleAdd(tokens: ["add", "1965", "Dune"], library: &library)
print(r3) // success()

And here it is clearly visible: "[add] end" is printed always. You no longer need to remember “did I definitely print end in all guard branches?”

8. defer as technical cleanup: resetting a flag and flow scheme

defer as “resetting a flag”: protecting ourselves from forgetfulness

Besides logs, defer is perfect for the situation where “a mode was enabled during execution, and then must be turned off.” A classic example is the isProcessing flag, which protects against rerunning a command (or simply helps debugging).

import Foundation

func runCommandSafely(_ action: () -> Void) {
    var isProcessing = true
    defer { isProcessing = false }

    print("processing =", isProcessing) // processing = true
    action()
    print("processing =", isProcessing) // processing = true
}

runCommandSafely {
    print("work...")
}
// processing = true
// work...
// processing = true

Yes, this is a toy example, but the logic is serious: defer guarantees that “turning off the mode” will happen even if there is an early return or an error inside (if action were throws, we would structure it a bit differently).

A small flow diagram: why defer makes code shorter

Sometimes it is useful to think of defer as a single common tail for all exits. Below is a diagram where many exits converge into one “cleanup.”

flowchart TD
    A[Function start] --> B[defer: registered cleanup]
    B --> C{checks / guard}
    C -->|failed| R1[return/throw]
    C -->|passed| D[main work]
    D --> R2[return]
    R1 --> E[exit scope]
    R2 --> E[exit scope]
    E --> F[defer cleanup runs]

Fun fact: even low-level code likes defer

Although today we are not diving into unsafe pointers and manual memory management (and that is the right choice — nerves are more expensive), the fact itself is useful: defer is so practical that it is used even where forgetfulness is costly. For example, in one low-level reading example, data is allocated, and defer guarantees buffer release.

And in discussions of new language features, an idea appears: to use defer to guarantee restoration of a variable's state on any exit from scope, including errors.

We do not see this as “now let's rush to write unsafe code,” but as confirmation: defer is not a toy for textbooks, but a workhorse.

9. Typical mistakes when working with defer

Mistake #1: treating defer as “code at the end of a function” rather than “code on scope exit.”
Then confusion appears: why does defer inside do { } run earlier than you expected? Because do is a separate scope, and exiting it happens earlier than exiting the function. Things usually become clear if you mentally replace do with “a small function in your head.”

Mistake #2: placing defer after guard and then wondering why it did not run.
This is not a Swift bug and not a “broken defer,” it is execution logic: if execution never reached the defer line, it was not registered, and there is nothing to run. The practical conclusion is simple: if cleanup is always needed, declare defer as early as possible — right after the place where you “started work.”

Mistake #3: putting business logic into defer.
defer should be short and technical: log "end", reset a flag, restore state. If you hide part of the algorithm's meaning in defer, the code becomes unreadable: the main action seems to happen “after the end.” This is the kind of situation where you create your own quest of “why does it work this way?”

Mistake #4: forgetting the LIFO order of multiple defers.
When two or three defers appear in a function, a beginner may expect them to run “top to bottom.” In reality — the opposite. If you do not account for this, you can get a “strange log” or the wrong order of restoring temporary state. The fix is simple: keep in mind “like a stack,” and arrange defer so that the reverse order is natural.

Mistake #5: not considering that defer sees the current values of variables.
If in defer you print a variable and then change it — the already changed value will be printed. Sometimes that is what you want (logging the final state), and sometimes you wanted “what it was at the start.” Then make a snapshot in let snapshot = ... and use snapshot inside defer.

1
Task
Swift SELF, level 31, lesson 3
Locked
Polite Greeting
Polite Greeting
1
Task
Swift SELF, level 31, lesson 3
Locked
Two Cleaners
Two Cleaners
1
Task
Swift SELF, level 31, lesson 3
Locked
Splitting the loot
Splitting the loot
1
Task
Swift SELF, level 31, lesson 3
Locked
Guarded iteration
Guarded iteration
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION