When working with async Swift code in Swift 6, you’ll eventually hit this classic question:
“Is this code running on the main thread?”
You might try the old line:
print(Thread.isMainThread ? "is on Main Thread" : "not on Main Thread")
…and get hit with this Swift 6 compiler error:
‘Thread.isMainThread’ is unavailable from asynchronous contexts
“Work intended for the main actor should be marked with @MainActor; this is an error in the Swift 6 language mode.”
So what now?
Stop thinking in threads. Start thinking in actors.
In Swift Concurrency, the real question isn’t “which thread?”, it’s:
“Which actor am I on?”
- UI work must happen on the
MainActor
- Heavy work should run off the
MainActor
- Actor isolation is what protects your code — not threads.
Actors manage serial execution and data safety. Threads are implementation details.
Use MainActor.assertIsolated() to debug actor isolation
During development, you can assert that you’re on the MainActor
like this:
func updateUI() {
MainActor.assertIsolated("UI updates must happen on MainActor!")
self.title = "Loaded"
}
If you ever call this function from the wrong actor, it stops you immediately during development.
What it does:
- In Debug builds (Xcode default): Crashes immediately if you’re not on the
MainActor
- In Release builds: Does nothing (zero cost)
Want a hard crash in all builds? Use preconditionIsolated
MainActor.preconditionIsolated("UI must be touched on MainActor")
This crashes in Debug and Release. Use this when calling from the wrong actor would be a logic error in production.
What does the crash actually look like?
When the assertion fails, your app will just trap:

In the Xcode debugger navigator, you’ll see:
Task 1 Queue : com.apple.root.user-initiated-qos.cooperative (concurrent)
9 ContentView.fetchAndDecode(query:)
10 ContentView.load()
This tells you:
-
Queue ... (concurrent)
You are not on the MainActor - You are running on a background actor with
.userInitiated
QoS - This task is off the main thread and outside the UI actor
If you’re on the MainActor:
Queue: com.apple.main-thread (serial)
That queue name is your runtime hint about actor context.
What about custom global actors?
You can create your own actor — and use the same assertion methods:
@globalActor
actor ImageCacheActor {
static let shared = ImageCacheActor()
}
@ImageCacheActor
func mutateCache() {
ImageCacheActor.assertIsolated("must be on ImageCacheActor")
// safe to touch cache here
}
It works just like MainActor.assertIsolated
.
There’s no assertNotMainActor
, so if you want to make sure you’re not on main, you can drop in a MainActor.assertIsolated("should NOT be main")
as a temp check and see if it crashes — if it doesn’t, then you’re on main and now you know. You can also add a breakpoint and read the Task value from the debug navigator area.
Summary
Swift 6 enforces proper actor usage. Take it seriously — it saves you from race conditions and UI bugs. Forget Thread.isMainThread
. Ask instead:“Am I on the actor I expect to be on?”
During development:
- Use
assertIsolated
to catch mistakes early - Use Xcode’s Queue info to understand runtime actor context
- Use custom logs to visualize where hops are happening
Stay actor-aware, and let the compiler + runtime do the heavy lifting. 💪
You’re not just managing threads anymore — you’re designing concurrency.