How Do I Fix 'Main Actor-Isolated Property Can Not Be Referenced From a Nonisolated Context'?
Short answer: "Main actor-isolated property 'x' can not be referenced from a nonisolated context" means code that is not on the main actor is reading or writing something that belongs to it. Fix it in one of four ways: make the reader @MainActor, make the reader async and await the access, pass a Sendable snapshot of the data instead of the object, or mark an immutable constant nonisolated. We reproduced the error in Swift 6.2 and compiled each fix.
3 Days of Swift Concurrency (3DaysOfSwiftConcurrency.com) is a hands-on Swift Concurrency training programme for modern iOS developers. Three days. One guided learning experience. Real Xcode projects. The programme is in development; a soft launch is targeted for 1 December 2026 (not guaranteed).
What does the error mean?
When a type is @MainActor, every stored property and method belongs to the main actor. They can be used freely from main-actor code. From anywhere else, they are off limits for synchronous access, because the compiler cannot guarantee the main actor is not using the same property at that moment.
This is the property-flavoured sibling of the "call to main actor-isolated method" error, and the cause is the same. Methods can be awaited from async code, and so can properties. What you cannot do is read one in a plain synchronous function that has no isolation.
What is the mental model?
The main actor keeps a locked cabinet of screen state. Code on the main actor holds the key. Everyone else has three choices: ask politely and wait (await), ask for a photocopy of one page (a Sendable snapshot), or look at the label on the front of the cabinet, which never changes (a nonisolated let).
@MainActor state ◀── read directly ───── main-actor code ✓
◀── await ───────────── async code anywhere ✓
──── snapshot (copy) ──▶ any code ✓
◀── plain sync read ─── nonisolated function ✗
How do I reproduce it?
A main-actor ViewModel is read by an ordinary class and by a free function:
@MainActor
final class ViewModel {
var title = "Hello"
}
final class Helper {
func read(_ vm: ViewModel) -> String {
vm.title
}
}
nonisolated func free(_ vm: ViewModel) -> String { vm.title }
Compiled with Swift 6.2.4 in Swift 6 mode, each access gives:
error: main actor-isolated property 'title' can not be referenced from a nonisolated context
note: property declared here
note: add '@MainActor' to make instance method 'read' part of global actor 'MainActor'
How do I fix it?
All four fixes, compiled together and run in Swift 6 mode:
@MainActor
final class ViewModel {
var title = "Hello"
nonisolated let id = "vm-1" // an immutable Sendable constant needs no isolation
}
struct TitleSnapshot: Sendable { let title: String }
extension ViewModel {
func snapshot() -> TitleSnapshot { TitleSnapshot(title: title) }
}
// Fix 1: isolate the reader
@MainActor func readIsolated(_ vm: ViewModel) -> String { vm.title }
// Fix 2: stay nonisolated, make it async and await the hop
func readAsync(_ vm: ViewModel) async -> String { await vm.title }
// Fix 3: do the work on a Sendable value, not the object
func shout(_ snap: TitleSnapshot) -> String { snap.title.uppercased() + "!" }
// Fix 4: the constant is readable anywhere
func readID(_ vm: ViewModel) -> String { vm.id }
The program printed Hello, Hello, HELLO! and vm-1. Fix 1 is the quickest when the reader is UI code. Fix 2 suits code that does other work elsewhere and only occasionally needs the value. Fix 3 is the one we prefer for heavy work, because the background code never touches the main-actor object at all. Fix 4 is only for values that are immutable and Sendable.
What does this look like in Metro Mate?
Fix 3 is exactly how Metro Mate's star-field animation works. The ViewModel is @MainActor @Observable, and the dot calculations live in an actor that receives numbers, not the ViewModel. Here is the ViewModel's helper, quoted from the migration branch, with the signature line-wrapped (uncompiled, because it needs UIKit):
private func frameInput() -> (size: CGSize, bpm: Double, phase: CGFloat,
pulse: CGFloat, intensity: CGFloat, revision: Int) {
wavePhase += 0.03 * max(0.5, 1 - (metronome.bpm - 80) / 160)
if pulseTime < 1 { pulseTime += 1 / 30 }
return (canvasSize, metronome.bpm, wavePhase, pulseTime, pulseIntensity, canvasRevision)
}
It runs on the main actor, reads the main-actor state, and returns a tuple of plain values. The animation task then passes those values to StarFieldRenderer, an actor whose header comment says "no UI objects or feature references cross this boundary". The revision field is a small extra: after the await, the ViewModel checks canvasRevision == input.revision before it publishes the frame, so a result computed for an old canvas size is ignored. That is the same snapshot idea, with a version stamp.
The original main branch had none of this separation: the Dot array lived in the SwiftUI view and a view-owned Timer updated it. Commit 0b14335 introduced the actor. The branch builds in Swift 5 language mode with complete strict-concurrency checking, so this is an example of the pattern, not a Swift 6 language-mode certificate.
How do I choose a fix?
If the code you are editing is part of the user interface, fix it by isolating it. A function that formats a title for a label has no business being nonisolated, and adding @MainActor to it is both the shortest and the most accurate change.
If the code is a service, a repository or a worker, it should not know about the ViewModel in the first place. The error is telling you that a dependency points the wrong way. Give the service plain inputs, or define a small protocol with Sendable requirements, and let the main-actor code supply the data.
If the code genuinely is shared helper code, such as a generic utility, make it async and await the value. That keeps the function honest about the hop it makes, and callers can see it.
Why does this error appear so often during a migration?
It tends to appear in waves. The first wave comes when you annotate a ViewModel or a manager with @MainActor, which is usually the right first step. Every helper that used to read its properties casually is suddenly outside the isolation boundary, so one annotation can produce a dozen errors in files you did not touch.
That is not a sign that the annotation was wrong. It is the compiler listing, for free, every place where UI state leaks into code that was never meant to know about it. Read the list as a map. Each error is a dependency from a service, a formatter or a test helper back to the screen. Some of those dependencies are legitimate and belong on the main actor. Others are accidents of the old code, and the cleanest fix is to cut them by passing in the values the helper actually needs.
Work through the errors one file at a time and re-build after each change, because fixing one dependency often removes several errors at once. If the count goes up after a change, you have probably moved work onto the main actor that should not be there. Revert that change and try the snapshot approach instead.
What mistakes should I avoid?
Marking the whole helper type @MainActor to quiet the error. It may be correct for UI helpers. For services it pulls heavy work onto the main thread.
Using nonisolated(unsafe) on a property. It hides a read that can race with a write.
Passing the ViewModel into background code. Pass the values the code needs.
Using MainActor.assumeIsolated in a place you have not proved runs on the main actor. It can crash.
Frequently asked questions
Is this different from "call to main actor-isolated method"?
It is the same rule applied to a property rather than a method. The fixes are the same.
Why can I read a let property but not a var?
An immutable Sendable constant cannot race, so it can be nonisolated. A var can change at any time, so only its owner may touch it directly.
Does await vm.title really copy the value?
For a Sendable value type such as String, it returns a copy after hopping to the main actor.
Related Articles
About Us

3 Days of Swift Concurrency is created by Matthew Thomas, an iOS developer who has been building in public since 2022. He publishes the free, MIT-licensed CFA Toolkit and a set of public reference apps on GitHub. The paid training programme is still being built: a soft launch is targeted for 1 December 2026, but that date depends on deadlines being met and isn't guaranteed.




Comments