Fetching Remote Data With Async/Await In Recent And Older System Versions

Updated on February 12th, 2024

⏱ Reading Time: 8 mins

Performing asynchronous tasks has always been part of the game when building applications for Apple systems. Up until not that long ago, closures and completion handlers had been the main way to handle asynchronous code. A -definitely better- alternative came with the arrival of the Combine framework and its declarative API, where series of operators creating pipelines have been offering a different approach. But the real revolution in my humble opinion actually occurred in Swift with the async/await pattern, which literally transformed the way we write asynchronous code.

The entire async/await concept introduced in WWDC21 constitutes a big, but relatively easy chapter that is really worth the time to study. It’s the future in dealing with asynchronous work in Swift, so adopting it is not a matter of “if”, but “when”. With that in mind, I’m focusing on a quite specific technique in this post; how to fetch data from a remote server using async/await.

There are specific available APIs that make remote data fetching feel like a breeze. But even though async/await has been made available to system versions before iOS 15 and macOS 12 too, these APIs are not meant to be used there.

The good news in such cases is that we can still put async/await pattern in motion and use it with the old-good completion handlers. Doing so, as well as demonstrating how to fetch data in the most recent system versions, is what comes next in the post.

Preparing for data fetching

For the purposes of this post, the remote data will be cat facts taken from here. Visiting this link will take you to a page with sample data similar to what we’re going to get. In order to easily convert the fetched JSON data into values that we can use into a program, we’ll start by defining the following struct, which conforms to Decodable protocol:

The two stored properties in the above custom type match to the values in the JSON that we’ll be receiving soon.

In the frontend, we’ll be displaying a cat fact in the following pretty simple SwiftUI view:

Notice that there’s a toolbar button to tap on so as to initiate a fetching process. The actual button action is still missing, but we’ll get to that soon.

Fetching data in recent system versions

In order to better serve the needs of this small demonstration, we’ll create the following custom type:

In it, let’s define the following static method:

See that right after the name we append the async keyword to indicate that this method is going to do some asynchronous work. There is also the throws keyword that enables this method to throw errors in case they occur. The return value is an optional string, which will be either the actual fetched fact value, or nil if a certain condition that you’ll see next won’t be satisfied.

We’re going to perform four distinct actions in the above method:

  1. We’ll initialize a URLRequest object with the data endpoint URL.
  2. We’ll fetch the remote data using the URLRequest object and a new async/await API that URLSession provides.
  3. We’ll decode fetched data into a CatFact instance.
  4. We’ll return the fact string value from the method.

We’ll cover the first item in the list initially; we’ll create a URLRequest object:

The interesting part is coming now. We’ll perform a request using the brand new data(for:) method, which we’ll access through the shared instance of the URLSession class:

data(for:) is a method that works asynchronously, so we have to call it by prefixing it with the await keyword. Just because of that our own fetchCatFact() method becomes asynchronous too, and that’s why we marked it as async.

The above call to fetch the remote data may return errors. For that purpose, we also prefix it with the try keyword. However, instead of handling any error case here using a do-catch statement, we simply propagate it to the caller of fetchCatFact(); that’s why we added the throws keyword in the method’s signature.

The really cool thing here is that we’re performing an asynchronous task in a single line of code; there are no completion handlers, nor we are complicating the code flow.

The return value of the above is a tuple with the first value being the actual fetched data (JSON as a Data object), and the second being a URLResponse object. If we are not interested in the response, we can replace it with an underscore like so:

With the remote data becoming available that easy to us, we can move on and decode it into a CatFact object:

Decoding can throw errors too, so we necessarily prefix it with the try keyword.

Finally, we can return the actual fact string value from the catFact object:

That’s all it takes to fetch remote data if no other configuration is required! The entire method is this:

Initiating data fetching

After having implemented an asynchronous method capable of fetching data from a specific remote URL, the next move is to make use of it. In this example, this is something that will take place in the toolbar button shown previously; its action closure is empty, and this is exactly where the next piece of the puzzle fits.

In order to call the asynchronous fetchCatFact() method, it’s necessary to create a new Task instance. We’ll provide that task with a closure, where we’ll include all the asynchronous work that has to be done; that is, to call the fetchCatFact() and eventually get a random string to display.

Prefixing fetchCatFact() with the await keyword is mandatory, since it’s an asynchronous method and we have to wait here until it finishes its job. The fetchedFact constant will be containing either an actual string value, or it’ll be nil.

We can now update the local fact state property and display the cat fact. But since this is an action that will affect the UI, it has to be performed on the main thread mandatorily:

The result of all the above is illustrated right next:

Fetching cat facts by tapping on the toolbar button and displaying the fetched fact in a text view.

Async/await and URLSession in older system versions

Unfortunately, iOS 15 and macOS 12 are the minimum required operating system versions that support the data(for:) URLSession method. However, mixing async/await and prior system versions remains on the table, as they can still be combined together.

In such cases, the actual work of fetching remote data is still going to take place in methods with completion handlers. However, with just an additional step, we can avoid calling those directly, and start using new async methods instead.

Suppose that we have the following method in the DataFetcher type:

The Result type in the completion handler will give back either an optional string value, the fact text, or an error, if one has occurred. Inside the method, the first step is to create a URLRequest object using the endpoint URL once again, and after that to initiate a data task.

In the data task’s closure we perform certain checks. If the returned error is not nil, then we pass it as argument for the failure case in the completion handler’s result. In the good scenario where the error is nil, then we check if the data has actually a value or not. To keep things simple in case data is nil, we consider the request successful but we provide a nil value instead of a string. Otherwise, we decode the fetched data to a CatFact object exactly as we previously did. In the end, we pass the resulting fact string value to the success case of the result.

Note: For simplicity reasons, we ignore the response that is also coming back after having performed the request.

Now, how exactly does async/await fit to all that?

The following couple of steps are going to provide an answer. We start by defining a new asynchronous method:

We’ll call here the following particular, new in Swift method, which works asynchronously and it will help us convert a closure based method to the async/await pattern. For that reason we’ll prepend it with the await keyword, as well as with the try, because it can also return errors:

Note: In case there are not any potential errors involved in the process, then we can use the alternative withCheckedContinuation(_:) method instead.

The above closure is the place to add any code that is going to be executed asynchronously. In this case, that’s going to be a call to the fetchCatFact(completion:) method:

In the completion handler of fetchCatFact(completion:), we’ll supply the result value as argument to the following method of the continuation object that resumes the asynchronous task and you can see right next:

Swift is going to take care of everything else behind the scenes. What really matters to us is that we won’t have to deal with fetchCatFact(completion:) again; we’ll be calling the fetchCatFact() that we just implemented.

I think it’s important to highlight here once again that doing all that is necessary if only we want to support older system versions where data(for:) is not available. Otherwise, the content presented in the previous parts is sufficient.

If you’d like to contain everything in a single method and follow the one or the other route depending on the operating system version, something like the following would work just fine:


The coming of async/await pattern in Swift is a game changer in the way we work with asynchronous tasks. Complicated code with completion handlers and various paths of execution that are hard to read, follow and debug, will soon belong to past. As a word of notice, be cautious in case of refactoring existing codebases, especially large projects. Make use of the async/await techniques gradually, confirming that each change does not break the normal execution of the app. That said, I hope you’ve found today’s post interesting and valuable to your own projects.

Thanks for reading, enjoy coding!

Stay Up To Date

Subscribe to my newsletter and get notifiied instantly when I post something new on SerialCoder.dev.

    We respect your privacy. Unsubscribe at any time.