In this lesson, we’ll go over the traditional asynchronous code and see how error prone and difficult it is to maintain.
Explaining the Code in NewsService
Open the file NewsService.swift, and look at the function latestNews(:)
.
This function utilizes asynchronous programming to fetch news articles from a remote server, handling errors and parsing the response accordingly.
func latestNews(_ handler: @escaping (Result<[Article], NewsServiceError>) -> Void)
The function takes a completion handler receiving a Result
type containing
either an array of Article
or an error of type NewsServiceError
.
Here’s a breakdown of the code.
The function starts by defining a URLSession
data task.
This task is responsible for making a network request to a specified URL
to retrieve data asynchronously:
let task = URLSession.shared.dataTask(with: URLRequest(url: Self.newsURL)) { data, response, error in
...
}
After defining the data task, the function immediately starts it by calling task.resume()
.
This call initiates the network request:
task.resume()
The completion handler of the data task is a closure that takes three
parameters: data
, response
, and error
.
This closure is executed when the network request completes, whether successfully or with an error.
If there’s an error during the network request (such as no internet connection),
it logs the error using a logger and calls the completion handler with a
.failure
result containing a NewsServiceError.networkError
:
if let error {
Logger.main.error("Network request failed with: \(error.localizedDescription)")
handler(.failure(.networkError))
return
}
If the response from the server is not of type HTTPURLResponse
, it logs
an error and calls the completion handler with a .failure
result containing
a NewsServiceError.serverResponseError
:
guard let httpResponse = response as? HTTPURLResponse else {
Logger.main.error("Server response not HTTPURLResponse")
handler(.failure(.serverResponseError))
return
}
If the HTTP response status code is not in the 200-299 range (indicating success),
it logs an error and calls the completion handler with a .failure
result
containing a NewsServiceError.serverResponseError
:
guard httpResponse.isOK else {
Logger.main.error("Server response: \(httpResponse.statusCode)")
handler(.failure(.serverResponseError))
return
}
If the received data is nil
, it logs an error and calls the completion handler
with a .failure
result containing a NewsServiceError.serverResponseError
:
guard let data else {
Logger.main.error("Received data is nil!")
handler(.failure(.serverResponseError))
return
}
If the data is successfully received, it attempts to decode the JSON response
into a Response
object using JSONDecoder
:
do {
let apiResponse = try JSONDecoder().decode(Response.self, from: data)
Logger.main.info("Response status: \(apiResponse.status)")
Logger.main.info("Total results: \(apiResponse.totalResults)")
handler(.success(apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }))
} catch {
Logger.main.error("Response parsing failed with: \(error.localizedDescription)")
handler(.failure(.resultParsingError))
}
If the decoding fails, it logs an error and calls the completion handler with
a .failure
result containing a NewsServiceError.resultParsingError
.
Finally, if the decoding succeeds, it filters the articles in the response
to remove those where either the author or the URL to the image is nil
.
It then calls the completion handler with a .success
result containing
the filtered articles.
Debugging the Code
To see how the code execution flows, put a breakpoint on the following lines.
The first one on the task definition:
let task = URLSession.shared.dataTask(with: URLRequest(url: Self.newsURL)) { data, response, error in
The second one on the first line of the completion handler:
if let error {
The third one on the resume
task at the end of the function:
task.resume()
Start debugging the code by clicking the Run button or by selecting Run in the Product menu.
Once the app is launched in the simulator, tap Load Latest News. The first breakpoint is hit, and the execution pauses. In the Debugger navigator pane, you can see that the function is executed on the main thread.
Now, click Continue Program Execution. The execution continues, and the third breakpoint is hit.
The execution is still on the main thread.
Click Continue Program Execution again. After some time, once the server responded to the network request, the execution goes back to the second breakpoint.
You can now see that the execution is on a secondary thread.
Click Continue Program Execution again. The app presents the news articles.
Note that the caller of latestNews(:)
, the function fetchLatestNews()
in NewsViewModel
, is responsible for parsing the result and updating the
news
variable on the main thread:
func fetchLatestNews() {
news.removeAll()
newsService.latestNews { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success(let articles):
self?.news = articles
case .failure:
self?.news = []
}
}
}
}
From the debug session, you can see the complexity of following the execution with the completion handler.
The execution flow is not linear, and the code is error-prone.
No Help From the Compiler
As an example of how easy it is to compromise the code functionality, comment out
the final call of the completion handler in the function latestNews(:)
in NewsService
:
//handler(.success(apiResponse.articles.filter { $0.author != nil && $0.urlToImage != nil }))
Turn off the debugger, and rerun the program.
Once the app is launched in the simulator, tap Load Latest News.
Nothing happens! You have no errors and no results.
This is a pretty straightforward case with a prominent error. But think about how difficult it is to spot errors like this when you have nested completion call handlers and forget to call the completion handler in one of the rare error cases.
Async/await will streamline the code execution flow and help you manage these subtle errors by checking at compile time that your code always returns a result.