Debugging Swift Concurrency: “Am I on the Main Actor?” (Not the Main Thread)

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.

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form