Is it necessary to always use async in all functions when await is used in Swift

Viewed 59

I have the following example in Swift:

func call1() { call2() }
func call2() { call3() }
func call3() { call4() }
func call4() { call5() }
func call5() { print("finish") }

If I want to make the call5() an async function, is it mandatory to change all other calls to async to make call5() to work properly?

2 Answers

Calling an async function requires a matching await. To make it work in a sync context you need to wrap the call in a task.

Task { await call5() }

This will spawn a concurrent task on the current thread.

Here is the WWDC talk with more details about structured concurrency. https://developer.apple.com/videos/play/wwdc2021/10134/

You don’t have to make them all async, but you might consider doing so. Specifically, by making them all asynchronous, you enjoy two benefits:

  • The caller of call1 can now know when the whole sequence is done, all the way down to call5.

  • If the caller of call1 gets canceled, structured concurrency will make sure that the cancelation is automatically propagated all the way down to call5.

So, you have two options:

  1. Leave call1-call4 synchronous, but have call4 invoke call5 with Task { … }. As described above, the caller of call1 loses the ability to know when call5 is done and you cannot enjoy automatic cancelation propagation:

    func call1() { call2() }
    func call2() { call3() }
    func call3() { call4() }
    func call4() { Task { await call5() } }    // opts out of structured concurrency
    func call5() async { … }
    
  2. You can make them all asynchronous, so that the caller of call1 can know when the whole sequence is done and enjoy automatic cancelation propagation.

    func call1() async { await call2() }
    func call2() async { await call3() }
    func call3() async { await call4() }
    func call4() async { await call5() }
    func call5() async { … }
    

In short, neither approach is necessarily right or wrong: It depends upon precisely what call5 is doing asynchronously.

Also, if refactoring a legacy code base, you don’t have to jump to option 2 immediately, but rather you can start with option 1, and slowly progress to option 2 over time. See WWDC 2021 video Swift concurrency: Update a sample app for examples of how you might do this migration over time.

Related