How to make a completionHandler much safer


You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿


Can you guess what’s dangerous with this code?

Yes, this code follows the old pattern of using a completionHandler to deal with an asynchronous event, but that’s not the problem I have in mind!

Here’s what’s dangerous: code that calls the function fetchData() expects that the completionHandler will always be called at some point, either with the data or with an error.

However, there’s absolutely nothing that guarantees all code paths will indeed eventually call the completionHandler!

If we forget to call the completionHandler in one of the code paths, the code will still build successfully and that’s quite a problem!

However, a simple refactor can make this code much safer 😌

Here are the steps!

First, we declare a let constant that will store the result:

Notice that we don’t set the value of the constant immediately.

Then, we use a defer statement to call the completionHandler:

This defer statement guarantees that the completionHandler will always be called, just before the function returns.

And since the code inside the defer will be executed at the end of the function, we are allowed to use our constant result, because by then it will have a value.

Actually, the compiler will make sure that a value is assigned to result in every single possible code path!

If we forget to do so, then the compiler will throw an error and the code won’t build:

Even better, since result is a let constant, the compiler will also report an error if we mistakenly set its value more than once:

And so with this simple trick, our code is now guaranteed to always call its completionHandler!

That’s all for this article, I hope you’ve enjoyed learning this new trick!

Here’s the code if you want to experiment with it:

// Before
import Foundation

func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        guard error == nil else {
            completion(.failure(error!))
            return
        }

        guard let data else {
            completion(.failure(NetworkError.noData))
            return
        }

        completion(.success(data))
    }
    .resume()
}

// After
import Foundation

func fetchData(url: URL, _ completion: @escaping (Result<Data, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        let result: Result<Data, Error>

        defer {
            completion(result)
        }

        guard error == nil else {
            result = .failure(error!)
            return
        }

        guard let data else {
            result = .failure(NetworkError.noData)
            return
        }

        result = .success(data)
    }
    .resume()
}
Previous
Previous

Do you know what translatesAutoresizing MaskIntoConstraints actually does? 🤨

Next
Next

How to give great answers to technical interview questions 👩🏽‍💻👨🏻‍💻