Must you opt-in to Swift 6.2’s Important Actor isolation? – Donny Wals


Printed on: September 11, 2025

Swift 6.2 comes with some attention-grabbing Concurrency enhancements. Probably the most notable modifications is that there is now a compiler flag that may, by default, isolate all of your (implicitly nonisolated) code to the primary actor. It is a large change, and on this publish we’ll discover whether or not or not it is a good change. We’ll do that by looking at a number of the complexities that concurrency introduces naturally, and we’ll assess whether or not shifting code to the primary actor is the (right) answer to those issues.

By the top of this publish, it is best to hopefully be capable to resolve for your self whether or not or not important actor isolation is sensible. I encourage you to learn by means of your complete publish and to rigorously take into consideration your code and its wants earlier than you soar to conclusions. In programming, the correct reply to most issues is determined by the precise issues at hand. That is no exception.

We’ll begin off by wanting on the defaults for important actor isolation in Xcode 26 and Swift 6. Then we’ll transfer on to figuring out whether or not we must always hold these defaults or not.

Understanding how Important Actor isolation is utilized by default in Xcode 26

If you create a brand new undertaking in Xcode 26, that undertaking could have two new options enabled:

  • World actor isolation is about to MainActor.self
  • Approachable concurrency is enabled

If you wish to be taught extra about approachable concurrency in Xcode 26, I like to recommend you examine it in my publish on Approachable Concurrency.

The worldwide actor isolation setting will routinely isolate all of your code to both the Important Actor or no actor in any respect (nil and MainActor.self are the one two legitimate values).

Because of this all code that you simply write in a undertaking created with Xcode 26 will likely be remoted to the primary actor (except it is remoted to a different actor otherwise you mark the code as nonisolated):

// this class is @MainActor remoted by default
class MyClass {
  // this property is @MainActor remoted by default
  var counter = 0

  func performWork() async {
    // this perform is @MainActor remoted by default
  }

  nonisolated func performOtherWork() async {
    // this perform is nonisolated so it is not @MainActor remoted
  }
}

// this actor and its members will not be @MainActor remoted
actor Counter {
  var rely = 0
}

The results of your code bein important actor remoted by default is that your app will successfully be single threaded except you explicitly introduce concurrency. All the pieces you do will begin off on the primary thread and keep there except you resolve you’ll want to go away the Important Actor.

Understanding how Important Actor isolation is utilized for brand new SPM Packages

For SPM packages, it is a barely totally different story. A newly created SPM Package deal is not going to have its defaultIsolation flag set in any respect. Because of this a brand new SPM Package deal will not isolate your code to the MainActor by default.

You possibly can change this by passing defaultIsolation to your goal’s swiftSettings:

swiftSettings: [
    .defaultIsolation(MainActor.self)
]

Notice {that a} newly created SPM Package deal additionally will not have Approachable Concurrency turned on. Extra importantly, it will not have NonIsolatedNonSendingByDefault turned on by default. Because of this there’s an attention-grabbing distinction between code in your SPM Packages and your app goal.

In your app goal, the whole lot will run on the Important Actor by default. Any capabilities that you have outlined in your app goal and are marked as nonisolated and async will run on the caller’s actor by default. So when you’re calling your nonisolated async capabilities from the primary actor in your app goal they are going to run on the Important Actor. Name them from elsewhere they usually’ll run there.

In your SPM Packages, the default is to your code to not run on the Important Actor by default, and for nonisolated async capabilities to run on a background thread it doesn’t matter what.

Complicated is not it? I do know…

The rationale for operating code on the Important Actor by default

In a codebase that depends closely on concurrency, you may must cope with loads of concurrency-related complexity. Extra particularly, a codebase with loads of concurrency could have loads of information race potential. Because of this Swift will flag loads of potential points (once you’re utilizing the Swift 6 language mode) even once you by no means actually supposed to introduce a ton of concurrency. Swift 6.2 is a lot better at recognizing code that is secure despite the fact that it is concurrent however as a normal rule you need to handle the concurrency in your code rigorously and keep away from introducing concurrency by default.

Let us take a look at a code pattern the place we’ve got a view that leverages a process view modifier to retrieve information:

struct MoviesList: View {
  @State var movieRepository = MovieRepository()
  @State var films = [Movie]()

  var physique: some View {
    Group {
      if films.isEmpty == false {
        Listing(films) { film in
          Textual content(film.id.uuidString)
        }
      } else {
        ProgressView()
      }
    }.process {
      do {
        // Sending 'self.movieRepository' dangers inflicting information races
        films = attempt await movieRepository.loadMovies()
      } catch {
        films = []
      }
    }
  }
}

This code has a difficulty: sending self.movieRepository dangers inflicting information races.

The rationale we’re seeing this error is because of us calling a nonisolated and async technique on an occasion of MovieRepository that’s remoted to the primary actor. That is an issue as a result of within loadMovies we’ve got entry to self from a background thread as a result of that is the place loadMovies would run. We even have entry to our occasion from within our view at the very same time so we’re certainly making a attainable information race.

There are two methods to repair this:

  1. Guarantee that loadMovies runs on the identical actor as its callsite (that is what nonisolated(nonsending) would obtain)
  2. Guarantee that loadMovies runs on the Important Actor

Choice 2 makes loads of sense as a result of, so far as this instance is anxious, we all the time name loadMovies from the Important Actor anyway.

Relying on the contents of loadMovies and the capabilities that it calls, we’d merely be shifting our compiler error from the view over to our repository as a result of the newly @MainActor remoted loadMovies is looking a non-Important Actor remoted perform internally on an object that is not Sendable nor remoted to the Important Actor.

Ultimately, we’d find yourself with one thing that appears as follows:

class MovieRepository {
  @MainActor
  func loadMovies() async throws -> [Movie] {
    let req = makeRequest()
    let films: [Movie] = attempt await carry out(req)

    return films
  }

  func makeRequest() -> URLRequest {
    let url = URL(string: "https://instance.com")!
    return URLRequest(url: url)
  }

  @MainActor
  func carry out(_ request: URLRequest) async throws -> T {
    let (information, _) = attempt await URLSession.shared.information(for: request)
    // Sending 'self' dangers inflicting information races
    return attempt await decode(information)
  }

  nonisolated func decode(_ information: Information) async throws -> T {
    return attempt JSONDecoder().decode(T.self, from: information)
  }
}

We have @MainActor remoted all async capabilities aside from decode. At this level we won’t name decode as a result of we won’t safely ship self into the nonisolated async perform decode.

On this particular case, the issue could possibly be fastened by marking MovieRepository as Sendable. However let’s assume that we’ve got causes that stop us from doing so. Perhaps the actual object holds on to mutable state.

We may repair our downside by really making all of MovieRepository remoted to the Important Actor. That approach, we will safely cross self round even when it has mutable state. And we will nonetheless hold our decode perform as nonisolated and async to stop it from operating on the Important Actor.

The issue with the above…

Discovering the answer to the problems I describe above is fairly tedious, and it forces us to explicitly opt-out of concurrency for particular strategies and finally a complete class. This feels improper. It looks like we’re having to lower the standard of our code simply to make the compiler completely happy.

In actuality, the default in Swift 6.1 and earlier was to introduce concurrency by default. Run as a lot as attainable in parallel and issues will likely be nice.

That is virtually by no means true. Concurrency is just not the perfect default to have.

In code that you simply wrote pre-Swift Concurrency, most of your capabilities would simply run wherever they have been known as from. In observe, this meant that loads of your code would run on the primary thread with out you worrying about it. It merely was how issues labored by default and when you wanted concurrency you’d introduce it explicitly.

The brand new default in Xcode 26 returns this conduct each by operating your code on the primary actor by default and by having nonisolated async capabilities inherit the caller’s actor by default.

Because of this the instance we had above turns into a lot less complicated with the brand new defaults…

Understanding how default isolation simplifies our code

If we flip set our default isolation to the Important Actor together with Approachable Concurrency, we will rewrite the code from earlier as follows:

class MovieRepository {
  func loadMovies() async throws -> [Movie] {
    let req = makeRequest()
    let films: [Movie] = attempt await carry out(req)

    return films
  }

  func makeRequest() -> URLRequest {
    let url = URL(string: "https://instance.com")!
    return URLRequest(url: url)
  }

  func carry out(_ request: URLRequest) async throws -> T {
    let (information, _) = attempt await URLSession.shared.information(for: request)
    return attempt await decode(information)
  }

  @concurrent func decode(_ information: Information) async throws -> T {
    return attempt JSONDecoder().decode(T.self, from: information)
  }
}

Our code is way less complicated and safer, and we have inverted one key a part of the code. As an alternative of introducing concurrency by default, I needed to explicitly mark my decode perform as @concurrent. By doing this, I be sure that decode is just not important actor remoted and I be sure that it all the time runs on a background thread. In the meantime, each my async and my plain capabilities in MoviesRepository run on the Important Actor. That is completely effective as a result of as soon as I hit an await like I do in carry out, the async perform I am in suspends so the Important Actor can do different work till the perform I am awaiting returns.

Efficiency impression of Important Actor by default

Whereas operating code concurrently can enhance efficiency, concurrency does not all the time enhance efficiency. Moreover, whereas blocking the primary thread is unhealthy we should not be afraid to run code on the primary thread.

Every time a program runs code on one thread, then hops to a different, after which again once more, there is a efficiency value to be paid. It is a small value often, however it’s a value both approach.

It is usually cheaper for a fast operation that began on the Important Actor to remain there than it’s for that operation to be carried out on a background thread and handing the end result again to the Important Actor. Being on the Important Actor by default signifies that it is rather more express once you’re leaving the Important Actor which makes it simpler so that you can decide whether or not you are able to pay the fee for thread hopping or not. I can not resolve for you what the cutoff is for it to be value paying a value, I can solely let you know that there’s a value. And for many apps the fee might be sufficiently small for it to by no means matter. By defaulting to the Important Actor you possibly can keep away from paying the fee by chance and I feel that is an excellent factor.

So, do you have to set your default isolation to the Important Actor?

On your app targets it makes a ton of sense to run on the Important Actor by default. It permits you to write less complicated code, and to introduce concurrency solely once you want it. You possibly can nonetheless mark objects as nonisolated once you discover that they have to be used from a number of actors with out awaiting every interplay with these objects (fashions are an excellent instance of objects that you’re going to most likely mark nonisolated). You should use @concurrent to make sure sure async capabilities do not run on the Important Actor, and you should use nonisolated on capabilities that ought to inherit the caller’s actor. Discovering the right key phrase can generally be a little bit of a trial and error however I usually use both @concurrent or nothing (@MainActor by default). Needing nonisolated is extra uncommon in my expertise.

On your SPM Packages the choice is much less apparent. When you’ve got a Networking package deal, you most likely don’t need it to make use of the primary actor by default. As an alternative, you may need to make the whole lot within the Package deal Sendable for instance. Or possibly you need to design your Networking object as an actor. Its’ solely as much as you.

When you’re constructing UI Packages, you most likely do need to isolate these to the Important Actor by default since just about the whole lot that you simply do in a UI Package deal ought to be used from the Important Actor anyway.

The reply is not a easy “sure, it is best to”, however I do assume that once you’re doubtful isolating to the Important Actor is an efficient default selection. If you discover that a few of your code must run on a background thread you should use @concurrent.

Observe makes good, and I hope that by understanding the “Important Actor by default” rationale you may make an informed choice on whether or not you want the flag for a particular app or Package deal.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles