Discovering Async/Await
Follow along on this journey, discovering the powerful world of async/await.
Streamlining Asynchronous Code with Async/Await
Start refactoring the latestNews(_:)
function with the new URLSession
’s
asynchronous API by replacing its content with the following code:
// 1. Asynchronous function uses the `async` keyword
func latestNews() async throws -> [Article] {
// 1. Execution pauses on the await until data is available
// or an error is thrown.
let (data, response) = try await URLSession.shared.data(from: Self.newsURL)
// 2. Once the network response is available, execution resumes here
guard let httpResponse = response as? HTTPURLResponse, httpResponse.isOK else {
Logger.main.error("Network response error")
throw NewsServiceError.serverResponseError
}
let apiResponse = try JSONDecoder().decode(Response.self, from: data)
Logger.main.info("Response status: \(apiResponse.status)")
Logger.main.info("Total results: \(apiResponse.totalResults)")
return apiResponse.articles.filter { $0.author != nil }
}
Here’s a brief breakdown of what the code does:
- It uses
URLSession.shared.data(from:)
to asynchronously fetch data. Theawait
keyword suspends the function’s execution until the data is available or an error is thrown during the network request. - Once the network response is received, the execution resumes and checks whether the response is successful; otherwise, it throws an error.
- If the response is successful, it attempts to decode the received JSON. If decoding fails, it throws an error.
- Finally, it returns an array of
Article
objects extracted from the API response, filtering the articles where theauthor
property is nil.
Don’t worry about understanding all the details: You’ll get to the details in the second part of this lesson. For now, focus on some more general facts, especially comparing this code with the previous one.
First of all, the function declaration is straightforward: The function
returns an array of articles and throws an error if an error occurs.
The word async
indicates to the system that it can suspend the execution
until certain asynchronous tasks are completed.
The execution flow is sequential, meaning that the instructions are executed from the top to the bottom, apart from the suspension point, where it’s paused. That makes the code easier to follow and to manage.
The compiler can now enforce that either a response or an error is returned for all the execution branches. With the completion handler, you were responsible for checking that the handler is called in every execution branch.
What lovely life improvements!
Now that you’ve met async/await, you’ll get into the details of how and when using the new asynchronous APIs.
Declaring Asynchronous Code with async
Identifying and marking asynchronous functions in Swift is essential for effectively managing concurrency and asynchronous operations in code.
With the introduction of the async
keyword in Swift, you can easily
denote functions that perform asynchronous tasks.
To identify and mark asynchronous functions, simply prepend the async
keyword before the function declaration.
As you did to make the function latestNews()
asynchronous, you add the async
keyword:
func latestNews() async throws -> [Article] {
...
}
If the function might also throw, the async
keyword precedes throws
.
The error management is also simpler than the completion handler,
where you used to wrap the result with the Result<Type, Error>
.
With async/await, the function returns the result type, such as an array of
Article
objects, or throws an error if something happens in the execution.
It’s as simple as that!
By adding the async
keyword, Swift recognizes that the function performs
asynchronous work and can be awaited within other asynchronous contexts.
This explicit marking enables the Swift compiler to perform optimizations
and enforce correctness checks related to asynchronous programming.
Here are the types of objects you can mark with the async
keyword:
-
Functions: Including global functions, methods, and closures.
-
Methods: Both instance and static methods in classes, structures, and enumerations can be marked as asynchronous by adding the
async
keyword. -
Initializers: Allowing for asynchronous setup or initialization tasks to be performed during object creation:
struct DataModel { let data: Data // Async initializer init(dataURL: URL) async throws { let (data, _) = try await URLSession.shared.data(from: dataURL) self.data = data } }
Note: De-initializers can’t be marked as asynchronous because they must execute synchronously and can’t suspend execution.
-
Accessor methods: Computed property getters can be marked with the
async
(and eventuallythrows
) keyword, enabling them to perform asynchronous operations when getting the property value:class NewsAPIService: NewsService { var latestArticle: Article? { get async throws { try await latestNews().first } } }
Note: Computed property setters can’t be marked with
async
. Only read-only computed properties can be marked as asynchronous.
Last but not least, protocols can also contain a declaration of asynchronous objects.
To reflect the change you made in latestNews()
, open the NewsService.swift
file, and update the definition of the NewsService
protocol as follows:
protocol NewsService {
func latestNews() async throws -> [Article]
}
Invoking Asynchronous Code with await
Now that you know how to declare asynchronous code, you’ll get into the details of how and when to invoke it.
As you saw refactoring the latestNews()
function, you use the await
keyword to invoke asynchronous operations.
When the await
keyword is encountered, the current function suspends its
execution, allowing other tasks to run concurrently, thus preventing
blocking the thread.
Meanwhile, the awaited operation continues its execution asynchronously,
such as fetching the articles from the remote server in the background.
Once the awaited operation is completed, the result (the array of Article
) becomes available.
At this point, the execution of the function resumes from where it left
off after the await
statement.
If the awaited operation throws an error, Swift automatically propagates
that error up the call stack, allowing you to handle it using familiar
error-handling mechanisms such as try-catch.
Invoking Asynchronous Functions in Synchronous Context
To complete your refactor, you need to call the new form latestNews()
.
Open the file NewsViewModel.swift, and change fetchLatestNews()
as follows:
func fetchLatestNews() {
news.removeAll()
// 1. Execution pauses on await
let news = try await newsService.latestNews()
// 2. Execution resumes with articles or an error is thrown
self.news = news
}
As you see, there’s no more syntactic sugar to decode the result type from the completion handler to check if you have errors. You now receive articles or manage the thrown error. Yay!
If you try building the project, you’ll receive an error saying: ‘async’ call in a function that does not support concurrency.
That’s because you’re trying to call an asynchronous function from a synchronous context, and that’s not allowed.
In these cases, you need a sort of “bridge” between the two words: Enter the Task
object.
Task
is part of the unstructured concurrency and lets you run asynchronous
code in a synchronous context.
Open the file NewsViewModel.swift, and change fetchLatestNews()
as follows:
func fetchLatestNews() {
news.removeAll()
Task {
// 1. Execution pauses on await
let news = try await newsService.latestNews()
// 2. Execution resumes with articles or an error is thrown
self.news = news
}
}
Here’s what’s happening in this code:
- Inside the function, the
Task
block starts an asynchronous task, allowing the synchronous functionfetchLatestNews
to return while waiting for theTask
result. - Within the
Task
block, theawait
keyword suspends the task execution until the asynchronous operation inlatestNews()
is complete. - Once the
latestNews()
operation completes, the result is assigned to thenews
variable, which triggers a UI update with the fetched articles. - You might want to provide proper error handling in case the service throws an error.
Finally, build and run, and you’ll still see articles coming, though this time using async/await.