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()
}
}