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()
}