With Xcode 27.0 and Swift 6.4, calling String(describing: #isolation) inside a nonisolated(nonsending) async function can deallocate the calling actor while it is still running. The next actor access then stops with EXC_BAD_ACCESS. Swift Issue #92799 identifies an ownership bug: Swift does not copy the implicit isolation value before passing it to a generic consuming argument. This article explains the crash, affected versions, root cause, safe workaround, and verification steps.
The runtime error and identifying behavior
stop reason = EXC_BAD_ACCESS (code=1, address=0x12fb)
Thread 4, queue = 'com.apple.root.default-qos.cooperative'
frame #0: test() at test.swift
frame #1: async_MainTQ0_ at test.swift
Command-line runs may report signal 11 and exit status 139. An especially useful clue is that the actor’s deinit executes earlier than its source lifetime permits.
Worker.deinit
1: Optional(test.Worker)
Process stopped: EXC_BAD_ACCESS
Minimal reproduction
actor Worker {
deinit { print("Worker.deinit") }
func run() async -> String {
await describeIsolation()
}
}
nonisolated(nonsending)
func describeIsolation() async -> String {
String(describing: #isolation)
}
func test() async {
let worker = Worker()
print("1:", await worker.run())
print("2:", await worker.run())
print("end of test()")
}
await test()
print("done")
Compile in Swift 6 language mode and run under LLDB:
xcrun swiftc -swift-version 6 -g test.swift -o test
lldb --batch -o run -k bt ./test
Worker.deinit runs during the first call, and the second worker.run() crashes. Both -Onone and -O reproduce the problem, so it is not limited to Debug builds.
Root cause
Inside a nonisolated(nonsending) function, #isolation is backed by an implicit @guaranteed parameter representing the caller’s isolation. The function borrows that value. Swift must create an owned copy before passing it to code that consumes the argument.
When #isolation is passed directly to a generic operation such as String.init(describing:), whose generic value is passed indirectly as @in, SILGen stores the value in an argument temporary without emitting retain_value. The callee consumes the temporary, decrementing the calling actor’s reference count one extra time.
The actor can therefore be destroyed while its method is still active. Its next use accesses freed memory and raises EXC_BAD_ACCESS.
Important: An explicit cast alone does not fix the ownership problem. The issue confirms that String(describing: #isolation as (any Actor)?) still over-releases. Bind the value to a local first so Swift generates the required copy.
Affected versions and conditions
- Reproduced with Xcode 27.0 (27A266a), Apple Swift 6.4, macOS 27.0.1, and Apple M4
- Swift 6.2.4, 6.3.3, and 6.4 release compilers reproduce the runtime crash
- Both
-Ononeand-Oare affected - A regular actor calls a
nonisolated(nonsending)function - The function passes
#isolationdirectly to a generic consuming argument such asString(describing:)orString(reflecting:) - An ordinary
nonisolatedfunction is affected whenNonisolatedNonsendingByDefaultsupplies the behavior
A custom global actor also loses references and eventually crashes in the reported tests. The MainActor test did not show a retain-count change, but do not generalize that observation into a safety guarantee; apply the same workaround consistently.
Swift Issue #92799 remains open as of October 4, 2026. No fixed Xcode or Swift version has been officially identified.
Recommended fix
1. Bind #isolation to a local variable first
This is the workaround verified by the official issue:
nonisolated(nonsending)
func describeIsolation() async -> String {
let isolation = #isolation
return String(describing: isolation)
}
The local binding causes SIL to emit the necessary balanced retain_value and release_value operations, plus the copy consumed by the generic call. The caller’s actor reference is no longer consumed accidentally.
2. Apply the same rule to logging helpers
Debug logging and tracing code are affected too. Do not pass #isolation directly into a generic formatting API.
nonisolated(nonsending)
func logIsolation() async {
let isolation = #isolation
let text = String(reflecting: isolation)
print(text)
}
3. Fix code using the upcoming feature
nonisolated func describeIsolation() async -> String {
let isolation = #isolation
return String(describing: isolation)
}
The local binding is also required when NonisolatedNonsendingByDefault implicitly changes the function’s calling convention.
Affected and verified-safe expression shapes
Expressions confirmed to over-release in the issue include:
String(describing: #isolation)
String(reflecting: #isolation)
String(describing: #isolation as (any Actor)?)
genericConsumingSink(#isolation as (any Actor)?)
Forms that behaved correctly in the reported tests include:
let isolation = #isolation
return String(describing: isolation)
// Return the isolation value directly.
return #isolation
// Pass to a non-consuming generic parameter.
nonConsumingSink(#isolation as (any Actor)?)
// Store in a concrete property.
return Box(actor: #isolation)
Instead of making application design depend on detailed compiler calling conventions, the safest readable rule is to bind #isolation to a local before any generic processing.
Related compile-time failure
Directly injecting #isolation into another optional can fail at compile time instead of crashing at runtime:
nonisolated(nonsending)
func currentIsolation() async -> (any Actor)?? {
.some(#isolation)
}
Have operand with incompatible ownership?!
Found ownership error?!
error: fatal error encountered during compilation
Bind the value locally before constructing the result. Note that Found ownership error?! can also have unrelated causes, including typed-throws bugs, so match the source pattern before applying this diagnosis.
How to verify the workaround
- Record the toolchain with
xcrun swiftc -version. - Add a log or breakpoint to the actor’s
deinit. - Use a minimal reproduction to confirm that the unpatched code deinitializes the actor during the first call.
- Bind
#isolationto a local value before the generic call. - Call the actor at least twice in both Debug and Release configurations.
- Run an Address Sanitizer test and a normal Archive build.
The corrected program should produce:
1: Optional(test.Worker)
2: Optional(test.Worker)
end of test()
Worker.deinit
done
Notes and pitfalls
- An explicit
(any Actor)?cast alone is not a workaround. - Changing optimization level does not avoid this bug.
- Deleting DerivedData or cleaning the build does not change the faulty ownership generation.
- The use-after-free can originate in apparently harmless logging code. Search every direct use of
#isolation. - Keep the local-binding workaround until a future Xcode version is officially or independently verified with the minimal reproduction.
Summary
If an actor crashes with EXC_BAD_ACCESS after String(describing: #isolation), Swift may be passing a borrowed isolation value to a generic consuming argument without copying it, over-releasing the actor. Bind #isolation to a local variable before using it. This is the safe workaround confirmed by the official issue, which remains open with no fixed Xcode version identified.
Official references
- Swift Issue #92799: String(describing: #isolation) over-releases the calling actor (reported October 1, 2026; open when checked October 4, 2026)
- Apple Xcode Release Notes
