# Actors conforming to protocols in default @MainActor isolation targets

**URL:** <https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821>\
**Category:** Using Swift\
**Tags:** concurrency\
**Created:** [September 29, 2026, 7:11pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821 "2026-09-29T19:11:07Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![mbrandonw](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mbrandonw/32/349_2.png) [@mbrandonw](https://forums.swift.org/u/mbrandonw)\
**Post date:** [September 29, 2026, 7:11pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/1 "2026-09-29T19:11:07Z")

</div>

I've come across some behavior in default `@MainActor` isolation targets that I can't explain. Consider the following protocol defined in a `@MainActor` module, and an actor that conforms to the protocol:

```swift
// Default main actor isolation module
protocol P {
  func perform() async
}
actor A: P {
  func perform() async {}
}

```

I was surprised this compiled. It seems that `P` is effectively `@MainActor P`, and so an arbitrary actor shouldn't be allowed to conform. And if I explicitly make `P` into `@MainActor`, that is what happens:

```swift
@MainActor protocol P {
  func perform() async
}
actor A: P { // 🛑 Actor 'A' cannot conform to global-actor-isolated protocol 'P'
  func perform() async {}
}

```

Now the first code snippet only compiles when `perform` is async, and I think for that reason there isn't really a soundness hole. If you were to erase an instance of `A` to an `any P` and then invoke `perform()`, you would be forced to `await` and so isolation to the actor can be guaranteed. If the code compiled with `perform()` being non-async, _then_ there definitely would be a soundness problem since you would be able to invoke `perform()` synchronously from a main actor context. So luckily that is not the case.

But things get weird when you move `A` to a separate module:

```swift
// Module P, default main actor isolation
protocol P {
  func perform() async
}

// Module A, default isolation doesn't matter
import P
actor A: P::P { // 🛑 Actor 'A' cannot conform to global-actor-isolated protocol 'P'
  func perform() async {}
}

```

Now I do get the diagnostic I expected in the first code snippet. So I'm not sure how to make sense of this. Is this mentioned in any of the proposals about default main actor isolation?

Part of me thinks that the first code snippet should be rejected since I thought a `protocol P` in a main actor module would be equivalent to `@MainActor protocol P`, but that does not seem to be the case. But then the other part of me could accept that the first code snippet is legit because there is not a real soundness hole.

So, does this code work as expected, or is there something missing from default main actor isolation?

---

<div class="post-metadata">

**Author:** ![aviva](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aviva/32/44327_2.png) [@aviva](https://forums.swift.org/u/aviva)\
**Post date:** [September 29, 2026, 10:06pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/2 "2026-09-29T22:06:27Z")

</div>

eek, I (think I) fixed this bug while lifting the prohibition on actors conforming to isolated protocols, but it hasn't made it into a release yet. It is a source breaking change for (wrong, mistakenly accepted) code like you show, so hopefully nobody is relying on this!

> <https://github.com/swiftlang/swift/pull/91147>
>
> We can allow actors conforming to isolated protocols if we prevent
> inferring th…e isolation of the protocol onto the actor. This leads to
> some small source breaks (as far as I know).
> 
> This will stop compiling in \`-default-isolation MainActor\`:
> 
> \`\`\`swift
> @MainActor func mainActorFn() {}
> 
> protocol P {}
> 
> actor A: P {
> static func s() { mainActorFn() }
> // ^ now nonisolated, can't call @MainActor
> }
> \`\`\`
> 
> This will stop compiling in Swift 5 without
> \`DisableOutwardActorInference\`:
> 
> \`\`\`swift
> @MainActor func mainActorFn() {}
> 
> @propertyWrapper public struct W\<T: Sendable\> {
> @MainActor public var wrappedValue: T
> public init(wrappedValue: T) { self.wrappedValue = wrappedValue }
> }
> 
> actor A {
> @W var x: Int = 0
> static func s() { mainActorFn() }
> // ^ now nonisolated, can't call @MainActor
> }
> \`\`\`
> 
> There may be other cases relying on inferring a global actor onto an
> actor that will be broken by this change... but they are (probably)
> extremely incorrect, we reject the global actor annotation on an actor
> in a \`.swiftinterface\` during typechecking. We should not accept code
> that we later reject when typechecking the interface. Added an assertion
> that we don't do this.
> 
> Allowing these conformances when it is safe makes actors more useful.
> I suspect most practical uses are for async requirements, which can hop
> isolations if they don't transfer non-Sendable state.
> 
> I'm not sure if lifting this restriction means we should allow
> \`nonisolated init\` on actors, to allow them to avoid inits isolated to
> a global actor (a \`nonisolated init\` can witness \`@MainActor init\`). We
> already accept this code and infer an isolation for the actor init:
> 
> \`\`\`swift
> protocol P { @MainActor init() }
> actor A: P {}
> \`\`\`
> 
> Preventing init off MainActor:
> 
> \`\`\`swift
> nonisolated func noniso() {
> A() // error!
> }
> \`\`\`
> 
> And we don't allow preventing that isolation with \`nonisolated\`.
> Separate issue I guess.
> 
> Resolves rdar://183069416
> Resolves #90927

Your intuition is right that the async case is safe, that is what we will allow in the next release.

Let me know if I didn't read your post carefully enough and missed some nuance, but there are bugs where default isolation is applied to things, but "sneak past" the checking the compiler would do if it was written explicitly. There was a bug like this for protocols, where the protocol could get an isolation from the module default that could be inferred onto the actor, and actors were only prohibited from conforming to protocols that had their isolation written explicitly in source. When the default is inferred onto a protocol that is public, it is written into the interface, and the compiler sees it and forbids the conformance.

only [your async example (godbolt)](https://godbolt.org/z/xfscdM555) should compile with `-default-isolation MainActor -language-mode 6`. [`sync` gets a compile error (godbolt)](https://godbolt.org/z/Yr6TxaGz9).

---

<div class="post-metadata">

**Author:** ![mbrandonw](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mbrandonw/32/349_2.png) [@mbrandonw](https://forums.swift.org/u/mbrandonw)\
**Post date:** [September 30, 2026, 1:24pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/3 "2026-09-30T13:24:09Z")

</div>

Hi @aviva, thanks for the info!

That PR does seem related to this, but there's a chance that there is still some undefined behavior lurking.

This code snippet compiles fine in 6.4 ([godbolt](https://godbolt.org/z/eznT6naGK)):

```swift
// Default main actor isolation module
protocol P {
  func perform() async
}
actor A: P {
  func perform() async {}
}

```

And it sounds like this is expected, correct?

This code snippet produces an error in 6.4 ([godbolt](https://godbolt.org/z/EzMfTfqYq)):

```swift
// Default main actor isolation module
protocol P {
  func perform()
}
actor A: P {
  func perform() {}
}

```

And that definitely is expected because otherwise a data race would certainly be possible.

The part that confuses me is that this code does _not_ compile in 6.4 ([godbolt](https://godbolt.org/z/Yqcs6aE1W)):

```swift
@MainActor protocol P {
  func perform() async
}
actor A: P { // 🛑 Actor 'A' cannot conform to global-actor-isolated protocol 'P'
  func perform() async {}
}

```

The only difference in this snippet form the compiling example is that `P` has been explicitly marked as `@MainActor`. But is `protocol P` not the same as `@MainActor protocol P` in a main actor module?

And further, this does not compile in Swift 6.4:

```swift
// Module P, default main actor isolation
protocol P {
  func perform() async
}

// Module A, default isolation doesn't matter
import P
actor A: P::P { // 🛑 Actor 'A' cannot conform to global-actor-isolated protocol 'P'
  func perform() async {}
}

```

The only difference here from the compiling example is that the protocol has been put in a separate conformance from the actor.

So it seems that when `P` is used within its defining module it's not _really_ interpreted as `@MainActor P`, but outside the module it is. Is that behavior expected or documented, or described in the proposal?

---

<div class="post-metadata">

**Author:** ![aviva](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aviva/32/44327_2.png) [@aviva](https://forums.swift.org/u/aviva)\
**Post date:** [September 30, 2026, 6:09pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/4 "2026-09-30T18:09:03Z")

</div>

The PR I linked did not make it into 6.4 (afair); try your examples with the nightly compiler? It should do what you expect. 6.4 still had the bug I described.

---

<div class="post-metadata">

**Author:** ![mbrandonw](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mbrandonw/32/349_2.png) [@mbrandonw](https://forums.swift.org/u/mbrandonw)\
**Post date:** [September 30, 2026, 9:24pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/5 "2026-09-30T21:24:26Z")

</div>

Sorry, I should have downloaded the nightly and tried myself. It is now true that all 3 variations of my code compile:

```swift
// Default main actor isolation module
protocol P {
  func perform() async
}
actor A: P {
  func perform() async {}
}

```

```swift
@MainActor protocol P {
  func perform() async
}
actor A: P {
  func perform() async {}
}

```

```swift
// Module P, default main actor isolation
protocol P {
  func perform() async
}

// Module A, default isolation doesn't matter
import P
actor A: P::P {
  func perform() async {}
}

```

And so it's at least consistent! Thanks for the help.

My only last question is this documented? At first it doesn't seem to make much sense to conform an actor to a `@MainActor` protocol, after all a custom actor is definitely not the main actor. But if all the requirements happen to async, then that distinction can be blurred? And if one deals with an instance of the actor as an erased `any P`, then will Swift hop to the main actor and then instantly hop to the actor's isolation? It's definitely useful and greatly improves the ergonomics of main actor modules, but I just want to make sure I'm understanding the details correctly. Thanks!

---

<div class="post-metadata">

**Author:** ![aviva](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/aviva/32/44327_2.png) [@aviva](https://forums.swift.org/u/aviva)\
**Post date:** [September 30, 2026, 10:17pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/6 "2026-09-30T22:17:42Z")

</div>

Not documented anywhere, no. The Swift book is unfortunately pretty behind the details of Swift concurrency, but hopefully this can change! Maybe this forum thread will come up in search results.

Part of my motivation to fix that issue was to get rid of this userdoc, which espoused the idea of "isolated protocols": [[Concurrency] Remove prohibition on actors conforming to isolated protocols by a-viv-a · Pull Request #91147 · swiftlang/swift · GitHub](https://github.com/swiftlang/swift/pull/91147/changes#diff-48ee969b3112edc6065d8d8210c50b79c93a0c02c7ae90f2ceefc9ec0922f2b0)

> [@mbrandonw](#):
>
> At first it doesn't seem to make much sense to conform an actor to a `@MainActor` protocol, after all a custom actor is definitely not the main actor.

In my opinion\[1\] there is no such thing as a `@MainActor` protocol or an isolated protocol. There are protocols, which can require particular isolations for their requirements, that their conformers are `Sendable` / `SendableMetatype`, and so on. But `@MainActor protocol P` does not describe an isolated protocol. It does the following things:

- propagate to the requirements of the protocol (and its extensions, and refining protocols) unless a different isolation is specified

```swift
@MainActor protocol P {
  /* @MainActor */ func perform()
}

```

- provide an inference hint, to give isolation to conformers on the primary declaration, that don't specify an isolation

```swift
/* @MainActor */ struct S: P { /* ... */ }

```

This isn't _totally_ different from isolation on a type... `@MainActor struct S` can be overridden with a specified isolation on its members / methods in the same way. However, isolation on a type provides `Sendable`, applies to storage, and _will_ run in the specified isolation. Compare that to an isolated requirement, where a witness can be `nonisolated`, or even have a different isolation for `async` requirements!

Rambling: I suspect if [isolated conformances](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md) had been designed sooner, we may not have this inference or propagation behavior, and instead `@MainActor protocol P` (or some other, more clear syntax) would _only_ suggest isolated conformance by default. Protocol requirements would have no isolation at all, and isolated conformances would be used instead of isolation on requirements. With isolated conformance inference (`InferIsolatedConformances`) I think there is a lot less motivation to put isolation on requirements, since global actor isolated types automatically have isolated conformances. Protocols that do know most uses are main actor would be better served by this behavior, since they could be used in "usually main actor" projects without precluding `nonisolated` conformances. This would be less expressive in some cases, but I think a lot simpler.

All of that to say... nothing wrong with actors conforming to protocols when they can satisfy the requirements! An instance of an actor has its own "instance" isolation whereas a global actor is one shared isolation, but when the requirement mandates the caller `await` the witness is free to hop to an arbitrary isolation.

```swift
protocol P {
  @MainActor func foo()
}

protocol Q {
  @MainActor func bar() async
}

struct S1: P, Q { // totally fine
  nonisolated func foo() { /* ... */ }
  nonisolated func bar() async { /* ... */ }
}

struct S2: Q { // also totally fine
  @OtherActor func bar() async { /* ... */ }
  // ^ call will await even if it is @MainActor,
  // so we can witness the req from any isolation
}

actor A: Q { // just as fine as S2
  /* "instance of A" isolated */ func bar() async { /* ... */ }
  // ^ fine to witness the req with this isolation too!
}

```

* * *

1. There are probably other coherent ways to understand our system. I think this way makes the most sense though.

---

<div class="post-metadata">

**Author:** ![mbrandonw](https://sea2.discourse-cdn.com/swift/user_avatar/forums.swift.org/mbrandonw/32/349_2.png) [@mbrandonw](https://forums.swift.org/u/mbrandonw)\
**Post date:** [September 30, 2026, 11:33pm UTC](https://forums.swift.org/t/actors-conforming-to-protocols-in-default-mainactor-isolation-targets/89821/7 "2026-09-30T23:33:28Z")

</div>

Perfect, that clears up things. Thanks!
