Async/await operations — a new way of writing asynchronous code

Barrage
6 min readAug 4, 2021

--

Async/await mechanism is the new way to write asynchronous code that you traditionally write with a completion handler (known as closure). Asynchronous functions allow asynchronous code to be written as if it were synchronous. For demonstration purposes, we used Xcode 13 beta with the Swift 5.5 version.

A new way of writing asynchronous code

Five main problems that async/await could solve are:

  • Pyramid of doom
  • Better error handling
  • Conditional execution of the asynchronous function
  • Forgot or incorrectly call the callback
  • Eliminating design and performance issue of synchronous API because of callbacks API awkwardness

Async await provides the mechanism for us to run asynchronous and concurrent functions in a sequential way which helps us make our code easier to read, maintain and scale, which are very important parameters in software development.

Async/await vs. completion handlers

First, we are going to show you how you would traditionally write functions with completion handlers. Everyone is familiar with this approach.

Don’t get us wrong, this isn’t a bad way of writing functions with completion handlers. We’ve been doing it like this for years on our projects; completion handlers are commonly used in Swift code to allow us to send back values after a function returns.

Still, once you have many asynchronous operations that are nested, it’s not really easy for the Swift compiler to check if there is anything weird going on in terms of any bugs introduced into your code.

For cleaner code, we made a new Swift file called NetworkManager.swift, and in this file, we are handling our requests. Our User is a struct with values of name, email, and username.

struct User: Codable {
let name: String
let email: String
let username: String
}
// MARK: fetch users with completion handler
func fetchUsersFirstExample(completion: @escaping (Result<[User], NetworkingError>) -> Void) {

let usersURL = URL(string: Constants.url)

guard let url = usersURL else {

completion(.failure(.invalidURL))
return
}

let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
if let _ = error {
completion(.failure(.unableToComplete))
}

guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
completion(.failure(.invalidResponse))
return
}

guard let data = data else {
completion(.failure(.invalidData))
return
}

do {
let users = try JSONDecoder().decode([User].self, from: data)
completion(.success(users))
} catch {
completion(.failure(.invalidData))
}
}
task.resume()
}

As you can see, a lot is going on here; it is pretty hard to debug, there is a lot of error handling for “just” fetching user data, and we ended up with a bunch of lines of code. Most programmers needed to write a couple of these in projects, which gets very repetitive and ugly.

In our ViewController.swift file, we decided to show data inside a table view, so we made one and conformed to its data source. Once we decided to call this function, it would look something like this:

//MARK: 1. example -> with completion handlers
private var users = [User]()

private func getUsersFirstExample() {
NetworkManager.shared.fetchUsersFirstExample { [weak self] result in
guard let weakself = self else { return }

switch result {
case .success(let users):
DispatchQueue.main.async {
weakself.users = users
weakself.tableView.reloadData()
}
case .failure(let error):
print(error)
}
}
}

Defining and calling an asynchronous function

Now, on the other hand, here is how you would write a function with async/await, which is doing everything like the example above:

//MARK: fetch users with async/await using Result type
func fetchUsersSecondExample() async -> Result<[User], NetworkingError> {
let usersURL = URL(string: Constants.url)

guard let url = usersURL else {
return .failure(.invalidURL)
}

do {
let (data, _) = try await URLSession.shared.data(from: url)
let users = try JSONDecoder().decode([User].self, from: data)

return .success(users)
}
catch {
return .failure(.invalidData)
}
}

An asynchronous function is a special kind of function that can be suspended while it’s partway through execution. This is the opposite of synchronous functions, which either run to completion, throw an error, or never return.

We are using two keywords, async and await.

We use the async keyword to tell the compiler when a piece of code is asynchronous. And with await keyword, we tell our compiler that it has the option of suspending function until data or error is returned; and to indicate where the function might unblock the thread, similar to other languages such as C# and Javascript.

Swift APIs, like URLSession, are also asynchronous.

But then, how do we call an async marked function from within a context that’s not itself asynchronous, such as a UIKit-based view controller?

What we’ll need to do is to wrap our call in an async closure, which in turn will create a task within which we can perform our asynchronous calls — like this:

//MARK: 2. example -> async/await with Result type
private func getUsersSecondExample() {
async {
let result = await NetworkManager.shared.fetchUsersSecondExample()

switch result {
case .success(let users):
DispatchQueue.main.async {
self.users = users
self.tableView.reloadData()
}
case .failure(let error):
print(error)
}
}
}

Result type was introduced in Swift 5.0, and its benefits are to improve completion handlers.

They become less important now that async/await is introduced in Swift 5.5, of course.

However, it’s not useless (as you can see in the example above) since it’s still the best way to store results.

Here is one more example of usage async/await without Result type, which is even cleaner approach:

//MARK: fetch users with async/await third example, without Result type
func fetchUsersThirdExample() async throws -> [User]{
let usersURL = URL(string: Constants.url)
guard let url = usersURL else { return [] }
let (data, _) = try await URLSession.shared.data(from: url)
let users = try JSONDecoder().decode([User].self, from: data)
return users
}

Making a call of an async function in our ViewController.swift file:

override func viewDidLoad() {
super.viewDidLoad()
configureTableView()
async {
let users = await getUsersThirdExample()
guard let users = users else { return }
self.users = users
self.tableView.reloadData()
}
}

//MARK: 3. example -> async/await without Result type
private func getUsersThirdExample() async -> [User]? {
do {
let result = try await NetworkManager.shared.fetchUsersThirdExample()
return result
} catch {
// handle errors
}
return nil
}

One more really good thing about the above example is that we can use Swift’s default error handling with do, try, catch, even when asynchronous calls are performed.

As you can see in both our examples, there is no weak self capturing to avoid retain cycles, and there is no need to update UI on the main thread since we have the main actor (@MainActor) who is taking care of that (the main actor is accessible only if you are using Swift’s new concurrency pattern).

And voilà, here is our result:

Conclusion

Async/await is just a part of Swift Concurrency options; Apple’s SDKs are starting to make heavy use of them. This offers a new way to write asynchronous code in Swift; the only drawback is that it’s not compatible with older operating system versions, and we’ll still need to interact with other code that doesn’t yet use async/await. We hope this article will help you better understand asynchronous programming in Swift.

If you want to learn more about this pattern, or about everything that was introduced with Swift concurrency, visit Apple’s official website and read their documentation. Additionally, you can check out these useful links:

And if you are interested in source code, visit my GitHub account.

--

--

Barrage
Barrage

Written by Barrage

We are a team of creative and talented individuals who build reliable, UX oriented, and custom-tailored digital products and provide real-time customer service.