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. Theawaitkeyword 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
Articleobjects extracted from the API response, filtering the articles where theauthorproperty 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
asynckeyword. -
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
Taskblock starts an asynchronous task, allowing the synchronous functionfetchLatestNewsto return while waiting for theTaskresult. - Within the
Taskblock, theawaitkeyword suspends the task execution until the asynchronous operation inlatestNews()is complete. - Once the
latestNews()operation completes, the result is assigned to thenewsvariable, 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.