Jan 15, 2026

Actor Isolation And Protocol Conformance Error — Swift 6.2

Jan 15, 2026 General

Cover Image

Swift 6.2 changes how actor isolation and protocols interact in a meaningful way. When you try to have an actor or global actor-isolated type satisfy a requirement that makes no promise about isolation, the compiler now rejects it explicitly to prevent potential data races and unsafe concurrent access. Protocols historically describe shape — methods and properties — without specifying where or how they can safely run. Actors introduce execution context and strict isolation, and when these two semantic models collide, Swift 6.2 is clear about that.

Main actor-isolated instance method cannot be used to satisfy a nonisolated protocol requirement

Actor-isolated property or method cannot satisfy nonisolated protocol requirement

These errors often look surprising because they show up in places where previously Swift would either let the code compile or only emit a warning. Now, Swift refuses to let a mismatched contract slip through silently, and that’s ultimately a good thing — it pushes you to make your API and concurrency contracts explicit. At the heart of the issue is this: a protocol requirement without any isolation annotation is considered nonisolated. That means callers can invoke it without crossing any executor boundaries. An actor, or a global actor annotation like @MainActor, does introduce an executor boundary. When these collide, the compiler will stop you early and force you to clarify intent. Swift.org

What “Protocol Shape Vs Isolation” Really Means

Protocols in Swift were designed long before actors existed. They define what something should do, not where. Actors and global actors define where something runs — this is a core part of Swift’s concurrency safety model. When you mix these without being explicit, the compiler will now refuse the ambiguity.

Think about it like this: a protocol requirement is a promise to the caller that they can invoke a method without thinking about threads or executors. If an implementation suddenly says “I live on the main actor,” callers must know that up front — otherwise they might call into a method expecting it to run anywhere. Swift 6.2’s stricter checks are just enforcing this contract.

A UI View Model Example That Trips Everyone

Here’s something many SwiftUI developers run into quickly:

protocol SettingsViewModel {
    func refresh()
}

At first this looks normal. But the moment you implement it like this:

@MainActor
final class SettingsViewModelImpl: SettingsViewModel {
    func refresh() {
        // update UI state
    }
}

Swift 6.2 emits the protocol conformance error. That’s because the protocol hasn’t said anything about actor isolation. Callers (in type theory land) could assume they can call refresh() from a background context — but your implementation says “only on main actor.”

With the annotation in place, you need to update the protocol to match intent:

@MainActor
protocol SettingsViewModel {
    func refresh()
}

Now the API contract explicitly says that any consumer of this protocol must be on the main actor. Code that calls refresh() from a background context will be forced to hop, making performance and safety visible up front.

Putting @MainActor on the implementation alone doesn’t update the protocol’s promise — and that’s the core reason the compiler rejects it.

One other way to solve this problem is to tell the compiler that SettingsViewModelImpl conforms to SettingsViewModel only when it’s on the Main thread.

final class SettingsViewModelImpl: @MainActor SettingsViewModel {
    func refresh() {
        // update UI state
    }
}

Why Synchronous Protocols Fail For Actors

Another pattern that surprises teams involves actors and synchronous protocol requirements.

Consider a simple cache protocol:

protocol Cache {
    func value(for key: String) -> Data?
    func insert(_ data: Data, for key: String)
}

Then you try to implement it with an actor:

actor MemoryCache: Cache {
    private var storage: [String: Data] = [:]

    func value(for key: String) -> Data? {
        storage[key]
    }

    func insert(_ data: Data, for key: String) {
        storage[key] = data
    }
}

In Swift 6.2 this is rejected because those synchronous methods are implicitly actor-isolated and the protocol describes them as nonisolated synchronous methods. That mismatch cannot be resolved.

The recommended fix is to make the protocol’s contract asynchronous:

protocol Cache {
    func value(for key: String) async -> Data?
    func insert(_ data: Data, for key: String) async
}

This reflects the reality that accessing an actor’s state may suspend and require an executor hop. Callers now must await, making the cost visible and keeping safety explicit.

This isn’t just about compilation — it’s about API design. Functions that can potentially cross an isolation boundary should almost always be async.

Default Actor Isolation And Protocol Hierarchies

Swift 6.2 introduces defaults for actor isolation in project templates that can subtly affect protocols. For example, if a target’s default actor isolation is set to @MainActor, unannotated protocols in that target may have inferred isolation behavior. This can show up in deeper protocol inheritance chains:

protocol P1 { func f() }
protocol P2: P1 {}

struct S1: P1 { func f() {} }
struct S2: P2 { func f() {} }

Under certain default isolation settings, S1 might compile while S2 fails with:

Conformance of ‘S2’ to protocol ‘P2’ crosses into main actor-isolated code and can cause data races

This happens because the default isolation can propagate differently through the hierarchy unless you explicitly annotate the protocols with the intended executor context. Adding @MainActor to the protocols clarifies the intended context for all conformers. Swift Forums

Synthesized Conformances Like Codable Can Bite Too

The problem isn’t limited to your own protocols. Library protocols like Codable, Hashable, etc., can be impacted when default isolation or global actors are involved.

For example, a Decodable conformance might trigger:

Main actor-isolated initializer ‘init(from:)’ cannot be used to satisfy nonisolated protocol requirement

This happens when the inferred isolation for your type (possibly from default actor isolation settings in your target) doesn’t match the protocol’s expectations. You solve this by explicitly specifying the intended isolation on the conformance or by restructuring where decoding happens so that it doesn’t violate the protocol’s assumed isolation domain. Stack Overflow

Existentials Erase Isolation Knowledge

One of the least obvious — and most frustrating — aspects of Swift 6.2’s concurrency model shows up when you start using protocol existentials. This is where code that feels obviously safe suddenly becomes more restrictive, more await‑heavy, or even harder for the compiler to reason about.

Let’s walk through a concrete, non‑cache example that mirrors real application architecture.

Imagine a feature that records analytics events. Some implementations are lightweight and synchronous, others batch events and serialize access using an actor.

protocol AnalyticsClient {
    func track(event: String) async
}

Now here’s an actor‑based implementation that protects internal state:

actor BatchedAnalyticsClient: AnalyticsClient {
    private var pendingEvents: [String] = []

    func track(event: String) async {
        pendingEvents.append(event)
    }
}

So far, everything is clean and explicit. The method is async, the actor owns its state, and the compiler knows exactly what executor is involved when you call it.

If you keep the concrete type, isolation knowledge is preserved:

let analytics = BatchedAnalyticsClient()

Task {
    await analytics.track(event: "screen_opened")
}

Swift knows analytics is an actor. The call is scheduled directly onto that actor’s executor, with no ambiguity.

Now introduce an existential:

let analyticsClient: any AnalyticsClient = BatchedAnalyticsClient()

Task {
    await analyticsClient.track(event: "screen_opened")
}

At runtime, this is still the same actor. But at compile time, Swift has lost critical information. any AnalyticsClient does not guarantee actor isolation. The protocol itself doesn’t say where track(event:) runs — only that it’s async.

From the compiler’s point of view, analyticsClient could just as easily be backed by:

struct FireAndForgetAnalyticsClient: AnalyticsClient {
    func track(event: String) async {
        print("Tracked:", event)
    }
}

This implementation has no isolation at all. Because both implementations satisfy the protocol, Swift must treat calls through the existential conservatively. It cannot assume actor isolation, and it cannot take advantage of executor‑specific knowledge.

This is what people mean when they say existentials erase isolation knowledge. The compiler is no longer reasoning about a specific actor; it’s reasoning about the weakest possible contract the protocol allows.

You can see the effect more clearly when isolation matters to correctness. Consider this helper:

func recordLaunch(using client: any AnalyticsClient) async {
    await client.track(event: "app_launch")
}

Even if you know at runtime that the client is actor‑backed, Swift cannot rely on that. The existential hides it. The compiler must assume a generic async boundary, not an actor hop, which limits optimization and makes reasoning about execution order harder.

Now compare that to a generic version:

func recordLaunch<C: AnalyticsClient>(using client: C) async {
    await client.track(event: "app_launch")
}

Here, Swift retains the concrete type information for C. If C is an actor, the compiler knows that. If it’s a plain struct, it knows that too. Isolation knowledge is preserved instead of erased.

There’s another subtle variant that catches people off guard: protocols that appear actor‑like, but aren’t annotated.

protocol UserSession {
    func refreshToken() async
}

You might implement it with an actor:

actor SecureUserSession: UserSession {
    func refreshToken() async {
        // touches secure, isolated state
    }
}

But if you pass this around as any UserSession, Swift treats it as potentially non‑isolated. The protocol never promised actor confinement, so the compiler can’t assume it.

If, however, the protocol itself is annotated:

@MainActor
protocol UserSession {
    func refreshToken() async
}

then even an existential like any UserSession carries executor information. In this case, isolation is not erased, because it lives on the protocol itself.

The takeaway is subtle but important: existentials don’t just erase type information — they erase isolation guarantees unless those guarantees are part of the protocol contract.

When executor behavior matters — performance, ordering, or safety — prefer generics or explicitly isolated protocols. Use any Protocol when you truly want abstraction and are willing to give up concrete isolation knowledge.

nonisolated Isn’t A Free Escape Hatch

Once developers understand that actor isolation is what’s blocking protocol conformance, the next instinct is often to reach for nonisolated. On the surface, it looks like the perfect solution: tell the compiler that a method doesn’t need isolation, satisfy the protocol, and move on.

This works — but only when it’s actually true. And that distinction matters far more in Swift 6.2 than it ever did before.

Let’s look at a realistic example.

Imagine a service responsible for formatting user-facing strings. The protocol looks harmless enough:

protocol UserFacingFormatter {
    func displayName(for userID: UUID) -> String
}

Now you decide to back this with an actor because the formatter depends on cached, mutable state:

actor ProfileFormatter: UserFacingFormatter {
    private var nameCache: [UUID: String] = [:]

    func displayName(for userID: UUID) -> String {
        nameCache[userID] ?? "Unknown"
    }
}

Swift rejects this conformance. The protocol requires a synchronous, nonisolated method. The actor provides an isolated one. So you try the obvious fix:

actor ProfileFormatter: UserFacingFormatter {
    private var nameCache: [UUID: String] = [:]

    nonisolated func displayName(for userID: UUID) -> String {
        nameCache[userID] ?? "Unknown"
    }
}

And now the compiler stops you for a different reason. nonisolated means this method may run outside the actor’s executor. Accessing nameCache — which is actor-isolated state — is no longer allowed. Swift correctly points out that this would be a data race.

This illustrates the core rule: nonisolated is a promise that the method does not depend on actor-protected state. It is not a way to “turn off” isolation temporarily.

Here’s an example where nonisolated is appropriate.

Consider an actor that owns configuration but also exposes some pure metadata:

protocol IdentifiableService {
    var identifier: String { get }
}
actor AnalyticsService: IdentifiableService {
    private let id = UUID()

    nonisolated var identifier: String {
        id.uuidString
    }
}

This works because identifier does not mutate state and does not depend on serialized access. The value is immutable, and the compiler can verify that it’s safe to access without hopping onto the actor.

Now let’s look at a more subtle misuse that compiles but leads to incorrect mental models.

protocol SessionStatusProvider {
    func isAuthenticated() -> Bool
}

You implement it using an actor:

actor AuthSession: SessionStatusProvider {
    private var token: String?

    nonisolated func isAuthenticated() -> Bool {
        token != nil
    }
}

This will not compile in Swift 6.2, and that’s intentional. Even though the logic looks simple, token is actor-isolated mutable state. Marking the method nonisolated would allow it to be called concurrently from anywhere, which defeats the entire purpose of the actor.

The correct fix here is not nonisolated, but changing the protocol to reflect reality:

protocol SessionStatusProvider {
    func isAuthenticated() async -> Bool
}

And then:

actor AuthSession: SessionStatusProvider {
    private var token: String?

    func isAuthenticated() async -> Bool {
        token != nil
    }
}

This version is honest. It admits that checking authentication state may require synchronization and must respect actor isolation.

Another pattern worth calling out involves computed properties.

Developers often try this:

protocol AppVersionProviding {
    var appVersion: String { get }
}
actor AppInfo: AppVersionProviding {
    private let version: String

    nonisolated var appVersion: String {
        version
    }
}

This works only because version is immutable. If version were mutable or derived from actor state that can change, Swift would correctly reject it.

The takeaway is subtle but critical: nonisolated does not mean “safe” — it means “independent of actor isolation.” The compiler enforces this strictly in Swift 6.2, and that enforcement is what prevents accidental data races.

If you find yourself wanting to use nonisolated to satisfy a protocol requirement that logically depends on actor state, that’s a design smell. The protocol is lying about its execution model, and Swift is forcing you to correct it.

In practice, experienced teams follow a simple rule: use nonisolated only for pure, immutable, or stateless behavior. For everything else, make isolation and asynchrony explicit in the protocol itself.

That may feel verbose at first, but it leads to APIs that are clearer, safer, and far easier to reason about under concurrency — which is exactly what Swift 6.2 is trying to push us toward.

Closing Thoughts

By making isolation explicit in your protocols, aligning async requirements with actor boundaries, and understanding how default isolation interacts with library patterns, you can avoid data races, communicate intent more clearly, and make your concurrency model robust and predictable.

Once you internalize these patterns, the compiler will start to feel like a design partner — catching ambiguity before it turns into subtle bugs in production.