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. The await 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 the author 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 eventually throws) 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 function fetchLatestNews to return while waiting for the Task result.
  • Within the Task block, the await keyword suspends the task execution until the asynchronous operation in latestNews() is complete.
  • Once the latestNews() operation completes, the result is assigned to the news 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.

See forum comments
Download course materials from Github
Previous: Asynchronous Programming Demo Next: Refactoring With Async/Await Demo