Which runtime checks fire when you misuse continuations, MainActor.assumeIsolated or dispatch queues, and which of them crash?

23.8.3 · L5 · Interview

Predict first

One program resumes a continuation twice, another never resumes it. What are the exit statuses?

// === main.swift ===
func f() async -> Int {
    await withCheckedContinuation { (c: CheckedContinuation<Int, Never>) in
        if CommandLine.arguments[1] == "double" { c.resume(returning: 1); c.resume(returning: 2) }
    }
}
let t = Task { print(await f()) }
try await Task.sleep(for: .milliseconds(200))
print("exiting")
  • 133 for the double resume, 0 for the leak
  • 0 for both
  • 133 for both

Predict first

assumeIsolated is called off the main thread. What happens at -Onone, -O and -Ounchecked?

// === main.swift ===
import Foundation
@MainActor func ui() { print("ui ok") }
DispatchQueue.global().async { MainActor.assumeIsolated { ui() } }
sleep(1)
print("unreachable")
  • It traps (133) in all three
  • It only traps at -Onone
  • It runs the closure

Mental model. A checked continuation is a one-use return ticket: using it twice stops the train, losing it just leaves the passenger waiting.

Several concurrency mistakes are caught at run time, and they differ in severity. Resuming a checked continuation twice traps with “SWIFT TASK CONTINUATION MISUSE: … tried to resume its continuation more than once” (status 133). Never resuming is quieter: the runtime logs “leaked its continuation without resuming it” but the program carries on with status 0, and the awaiting task simply never finishes, which is how silent hangs appear in async code. MainActor.assumeIsolated called from a background thread traps (133), and in the check it still traps with -O and -Ounchecked, so it is not a debug-only assertion. dispatchPrecondition(condition: .onQueue(.main)) traps likewise.

Debugging consequences: a leaked continuation shows up only in the log line (search for “MISUSE”) and as a task that never completes, so give continuation wrappers a single resume path and a timeout; a trap shows as EXC_BREAKPOINT in the crash report (Q461) with the runtime message in the application-specific information. Prefer await on an actor-isolated call to assumeIsolated, which exists for code you know runs on the actor but cannot prove to the compiler.

Double resume traps; a leak only logs.

It is a real runtime check, not an assertion that optimisation removes.

Common traps

  • Treating the leak message as harmless. The caller waits forever.
  • Using assumeIsolated to silence the compiler. A wrong guess crashes in production builds too.

They might follow up with

  • Unchecked continuation? No misuse checks at all.
  • Where is the message? stderr and the crash report.

Related questions

  • 23.5.1 How do you read a crash report, what do the exception types and Swift runtime messages mean, and how does…
  • 23.3.2 Why do some bugs appear only in Release builds, how do assert, precondition and fatalError differ, and how do…

← What does a Thread Sanitizer report look like, how does it report to…