The defer keyword in Swift


Greetings, traveler!

When I wrote the first article about async/await in the Swift Concurrency series, I used an example of an image downloader that uses a function to provide images from the Internet. That function handles the operation result with closures.

func loadImage(
    url: URL,
    _ completion: ((Result<UIImage, Error>) -> Void)?
) {
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard let data, let image = UIImage(data: data) else {
            completion?(.failure(error ?? ImageError.invalidData))
            return
        }
        
        completion?(.success(image))
    }
    
    task.resume()
}

One disadvantage of this approach was that we must remember to call this closure at the end of each scenario we process. If we forget to call the completion block in some cases, the compiler won’t tell us.

func loadImage(
    url: URL,
    _ completion: ((Result<UIImage, Error>) -> Void)?
) {
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        guard let data, let image = UIImage(data: data) else {
            // completion?(.failure(error ?? ImageError.invalidData)) ••• ✅ Compiled successfully
            return
        }
        
        completion?(.success(image))
    }
    
    task.resume()
}

This is definitely not a convenient and safe way to handle the operation result. So, I was wondering, is there a safer way to use closures? There is definitely at least one possible solution. Let’s take a look at it.

The defer keyword

The ‘defer’ statement in Swift executes a set of statements just before code execution leaves the current code block. So, if you want to do something regardless of the result of other operations, this keyword may come to the rescue.

Let’s look at the example of uploading images, but now we use the ‘defer’ keyword.

  1. Declare a let constant, storing the operation result.
  2. Then, use a ‘defer’ keyword to call the completion block inside the statement.

This statement guarantees that the completion block will be executed before this function returns.

func loadImage(
    url: URL,
    _ completion: ((Result<UIImage, Error>) -> Void)?
) {
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        let result: Result<UIImage, Error>
        
        defer {
            completion?(result)
        }
        
        guard let data, let image = UIImage(data: data) else {
            result = .failure(error ?? ImageError.invalidData)
            return
        }
        
        result = .success(image)
    }
    
    task.resume()
}

Since our result is a let constant, the compiler will only let us proceed if we assign it a value. This is a very convenient and safe approach.

func loadImage(
    url: URL,
    _ completion: ((Result<UIImage, Error>) -> Void)?
) {
    
    let task = URLSession.shared.dataTask(with: url) { data, _, error in
        let result: Result<UIImage, Error> // ❌ Compile error: Constant 'result' used before being initialized
        
        defer {
            completion?(result)
        }
        
        guard let data, let image = UIImage(data: data) else {
            // result = .failure(error ?? ImageError.invalidData)
            return
        }
        
        result = .success(image)
    }
    
    task.resume()
}

Multiple defers

Can you guess what the console will print out?

func printMethod() {
    defer {
        print("1")
    }
    
    defer {
        print("2")
    }
    
    defer {
        print("3")
    }
}

The correct answer is:

If multiple defer statements appear in some scope, they will be executed in reverse order.

Value capture

The ‘defer’ statements don’t capture references or current values of variables.

func captureMethod() {
    var title = "First title"
    
    defer {
        print(title)
    }
    
    title = "Second title"
    print(title)
}

So, the console will print:

Conclusion

There are other ways to use the ‘defer’ keyword. You can use it in many ways, for example, to call layoutIfNeeded() to update the constraints. Or hide the progress indicator after the network operation is executed. Although not very popular, this tool is quite useful. We just have to keep this in mind.