Thanks for being a part of WWDC25!

How did we do? We’d love to know your thoughts on this year’s conference. Take the survey here

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