How to migrate your code to Swift’s Async/Await - Part I

Co-author: Ignacio Cerviño LinkedIn

Completion handlers often make your code harder to read, follow, and maintain when working with asynchronous tasks. Completion handlers require manually managing the flow of your code, which can quickly become complex and error-prone, especially when dealing with multiple asynchronous operations. The result is often longer, less intuitive code. This article will explain the difference between async/await vs completion handlers and how to migrate your code step by step to a shorter, more precise, and safer code using Swift’s async/await features.

Please if you’d like to follow along, download the following repo:
https://github.com/ignaciocervino/AsyncAwaitMigrationDemo

Difference between Async/Await vs. Completion handlers

Here's a comparison of the two approaches to fetching dog image data—one using completion handlers and the other using async/await:

In this code:

  • Completion Handlers: The function takes a completion handler as a parameter. This handler is called with either a success or failure result, depending on whether the operation succeeds or encounters an error.

  • Manual Flow Control: The flow of the code is managed manually with multiple return statements, which can make it harder to follow, especially in more complex scenarios.

  • Error Handling: Errors are handled within the closure, and multiple guard statements are used to ensure that the data is valid before proceeding.

In this code:

  • Async/Await: The function is marked as async, and it returns a Result<DogImage, Error> directly, eliminating the need for a completion handler.

  • Simplified Flow: The code flow is much simpler and more linear, making it easier to read and understand. The use of try await handles asynchronous calls without requiring a separate closure.

  • Error Handling: Errors are caught using a do-catch block, which integrates seamlessly with the synchronous-like code flow provided by async/await.

These are the key Differences between the two:

  • Readability: The async/await version is more straightforward, as it eliminates the need for nested closures and multiple return statements.

  • Error Handling: With async/await, error handling is more integrated into the flow of the code, reducing the need for repetitive error-checking logic.

  • Code Simplicity: Async/await makes the code shorter and cleaner, focusing on the main logic without being cluttered by the mechanics of asynchronous execution.

Convert Completion Handlers to Swift’s Concurrency.

You don’t have to convert all of your code all at once; instead, you can use specific refactoring techniques to convert your code one piece at time.

Original Code Using Completion Handlers

Let's start with an example of a function that uses a completion handler:

Migration Steps

1 - Identify global variables and instance variables
In this example we don’t have neither global variables nor instance ones. This means that we won’t have any data races.

2 - Replace completion handler with supported async function in Foundation
Remove the completion handler in URLSession.shared.dataTask(with: url) and replace it with the new async function URLSession.shared.data(from: url)

Note: Remember to remove the .resume() since the new api doesn’t create a dataTask.

3 - Add await before the async function call

4 - Add the try before the await keyword
Capture the values into a constant. In this case data is the only value that we need.

5 - Handle errors from URLSession.shared.data(from: url) async function
There are two ways to proceed
A. Handle the error inside a do-catch block

B. Propagate the error by marking the function as throws

Note: we will use the option A, since we will handle errors in this function.

6 - Rearrange code
Insert the happy path code inside the do block, and the sad path in the catch block

Note: Since data is not optional, we don’t need to safe unwrap its value, so we can remove it.

7 - Convert the Function Signature:

7 - a. Change the function signature to use async and remove the completion handler parameter.

Note: Remember that we are returning Result<DogImage, Error>

7 - b. Remove completion handlers inside the function and use return instead

8 - Use the Task keyword to wrap and call the func fetchDogData() async -> Result<DogImage, Error> async function.

Notes:
- Remember to use await when calling the dogViewModel.fetchDogData() async function.
- Task works similar to DispatchQueue.async method. They will schedule the work in an arbitrary thread.
- Since we’re updating the UI, Task { @MainActor in is necessary to run the block in the main thread.

Conclusion

By following these steps and understanding when to use async/await, you can effectively migrate your code from completion handlers to modern Swift concurrency features. This not only makes your code more readable but also leverages the power of Swift's concurrency model to handle asynchronous operations more efficiently.

Previous
Previous

How to migrate your code to Swift’s Concurrency - Part II

Next
Next

#5 - TDD, Pair Programming, and Katas: Refactor with tests