Multiple async lets crash the app

Usage of multiple async lets crashes the app in a nondeterministic fashion. We are experiencing this crash in production, but it is rare.

0   libswift_Concurrency.dylib    	       0x20a8b89b4 swift_task_create_commonImpl(unsigned long, swift::TaskOptionRecord*, swift::TargetMetadata<swift::InProcess> const*, void (swift::AsyncContext* swift_async_context) swiftasynccall*, void*, unsigned long) + 384
1   libswift_Concurrency.dylib    	       0x20a8b6970 swift_asyncLet_begin + 36

We managed to isolate the issue, and we submitted a technical incident (Case-ID: 8007727). However, we were completely ignored, and referred to the developer forums.

To reproduce the bug you need to run the code on a physical device and under instruments (we used swift concurrency). This bug is present on iOS 17 and 18, Xcode 15.1, 15.4 and 16 beta, swift 5 and 6, including strict concurrency.

Here's the code for Swift 6 / Xcode 16 / strict concurrency: (I wanted to attach the project but for some reason I am unable to)

typealias VoidHandler = () -> Void
enum Fetching { case inProgress, idle }
protocol PersonProviding: Sendable {
    func getPerson() async throws -> Person
}

actor PersonProvider: PersonProviding {
    func getPerson() async throws -> Person {
        async let first = getFirstName()
        async let last = getLastName()
        async let age = getAge()
        async let role = getRole()
        
        return try await Person(firstName: first,
                                lastName: last,
                                age: age,
                                familyMemberRole: role)
    }
    
    private func getFirstName() async throws -> String {
        try await Task.sleep(nanoseconds: 1_000_000_000)
        return ["John", "Kate", "Alex"].randomElement()!
    }
    
    private func getLastName() async throws -> String {
        try await Task.sleep(nanoseconds: 1_400_000_000)
        return ["Kowalski", "McMurphy", "Grimm"].randomElement()!
    }
    
    private func getAge() async throws -> Int {
        try await Task.sleep(nanoseconds: 2_100_000_000)
        return [56, 24, 11].randomElement()!
    }
    
    private func getRole() async throws -> Person.Role {
        try await Task.sleep(nanoseconds: 500_000_000)
        return Person.Role.allCases.randomElement()!
    }
}
@MainActor
final class ViewModel {
    private let provider: PersonProviding = PersonProvider()
    private var fetchingTask: Task<Void, Never>?
    
    let onFetchingChanged: (Fetching) -> Void
    let onPersonFetched: (Person) -> Void
    
    init(onFetchingChanged: @escaping (Fetching) -> Void,
         onPersonFetched: @escaping (Person) -> Void) {
        self.onFetchingChanged = onFetchingChanged
        self.onPersonFetched = onPersonFetched
    }
    
    func fetchData() {
        fetchingTask?.cancel()
        fetchingTask = Task {
            do {
                onFetchingChanged(.inProgress)
                let person = try await provider.getPerson()
                guard !Task.isCancelled else { return }
                onPersonFetched(person)
                onFetchingChanged(.idle)
            } catch {
                print(error)
            }
        }
    }
}
struct Person {
    enum Role: String, CaseIterable { case mum, dad, brother, sister }
    
    let firstName: String
    let lastName: String
    let age: Int
    let familyMemberRole: Role
    
    init(firstName: String, lastName: String, age: Int, familyMemberRole: Person.Role) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
        self.familyMemberRole = familyMemberRole
    }
}
import UIKit

class ViewController: UIViewController {
    @IBOutlet private var first: UILabel!
    @IBOutlet private var last: UILabel!
    @IBOutlet private var age: UILabel!
    @IBOutlet private var role: UILabel!
    @IBOutlet private var spinner: UIActivityIndicatorView!
    
    private lazy var viewModel = ViewModel(onFetchingChanged: { [weak self] state in
        switch state {
        case .idle:
            self?.spinner.stopAnimating()
        case .inProgress:
            self?.spinner.startAnimating()
        }
        
    }, onPersonFetched: { [weak self] person in
        guard let self else { return }
        first.text = person.firstName
        last.text = person.lastName
        age.text = "\(person.age)"
        role.text = person.familyMemberRole.rawValue
    })

    @IBAction private func onTap() {
        viewModel.fetchData()
    }
}
we … referred to the developer forums.

Right. DTS is now trying to do most of our work in public; that way everyone can benefit from the results.

In this case there’s not much that we can do for you. Given that you’ve managed to reproduce this with a small test project, one that doesn’t use any obviously unsafe stuff, it’s unlikely to be a bug in your code. I recommend that you file a bug about this. Make sure to attach your test project and a sysdiagnose log including a crash report showing the issue.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Multiple async lets crash the app
 
 
Q