If you want the function to stay on whatever actor it was called on, you can use the nonisolated keyword.
Confusion
However based on my testing the nonisolated async function doesn't stay on the same actor (main actor).
nonisolatedasync function compute() doesn't run on the main thread (because MainActor.assertIsolated("Not isolated") causes a crash).
Am I missing something?
Questions:
Based on my interpretation what was stated in the video I was thinking that if already on the main actor and a non-isolated async function is invoked it would continue to run the main actor, but it doesn't seem to be the case. Am I missing something?
How to know if a async function is running on main thread or background thread?
Is it based on MainActor.assertIsolated("Not isolated")?
Project
Environment:
Xcode26.0 beta (17A5241e)
Toolchain: Swift 6.2 Development Snapshot 2025-06-14 (a)
Build Setting
Default Actor Isolation: Main Actor
Swift language version: 6
Code
Since the Default actor isolation is Main Actor, all run on Main Actor by default.
import Foundation
import Combine
// My assumption this would be Main actor because Default Actor Isolation: is Main Actor
@Observable
class Service {
// I thought this would also be Main actor based on what is mentioned in the video
// I am just saying based on by calling `MainActor.assertIsolated("Not isolated")`
nonisolated
func compute() async {
// MainActor.assertIsolated("Not isolated")
// When MainActor.assertIsolated is uncommented would crash implying
// it is not running on the main thread
for index in 0..<1000000 {
print("index: \(index)")
}
}
}
This mode will eventually become the new default mode, but for now it is opt-in. We think this behavior is what nonisolated async functions always should have been doing, but we need to be careful about rolling out the behavior change, thus staging it in this way.
@ktoso Thanks a lot!!! I was breaking my head over it.
I have a few questions:
In future once this feature goes live I suppose only we don't need to mention nonisolated(nonsending) instead could only mention nonisolated? For now we need to mention as nonisolated(nonsending)?
So is the following correct?
@concurrent will change it to background
nonisolated(nonsending) will continue on the same as the actor's executor
@MainActor will change it to main actor.
Actually seems simpler to understand concurrency with the above 3 rules. Not sure if I over simplified them and missed any cases.
Does that mean in different versions of Swift things would behave differently?
Well the "NonisolatedNonsendingByDefault" upcoming feature "is live" in the sense that you can opt into it.
There might be a future in which we make this the default, but it would be tied to a language mode. There's no concrete promise so far when or if that will be flipped to be the default though. But yeah that's the general direction.
this means "hop off calling actor executor; if there is a task executor, use that; if not, use the default global concurrent executor".
yes
yes
Yeah; so that's the "future state of the world". Until then we sadly have to deal with nonisolated and nonisolated(nonsending) but the latter will disapear in use in the long term as people enable the upcoming feature IMHO.
Thanks a lot @bjhomer, turning on "Approachable Concurrency automatically sets all of the flags under "Swift Compiler Upcoming features" which includes nonisolated (nonsending) by default
The video mentions it at the end but didn't realize it.
Thanks a lot @Douglas_Gregor for this video, really helped me understand concurrency and made it more accessible to me.
Thanks a lot to the Swift community for these new Swift concurrency features makes it so much better!!
Thank for your answer!
Could you help explain more about what is the different between nonisolated vs nonisolated(nonsending)
Iâm using Xcode 26 beta 5(MacOS 15.6) with settings:
Approachable Concurrency : Yes
Default Actor Isolation: MainActor
Strict Concurrency Checking: Complete
Swift Language Version: Swift 6
My code:
@MainActor final class MyViewModel: ObservableObject {
nonisolated//nonisolated(nonsending)
func test() async {
print("test is run on main: \\(Thread.isMainThread)")
}
}
And call the test function on my View:
var body: some View {
Text("")
.task {
await viewModel.test()
}
}
If the test function marked with nonisolated(nonsending)the result will be test is run on main: true
However, if I marked with nonisolated the result will be test is run on main: false
Is that mean nonisolated is equal behaviour with @concurrent?
Thanks!
As others have noted, the salient build setting is the ânonisolated(nonsending) by defaultâ. If you turn on âApproachable Concurrencyâ, this ânonisolated(nonsending) by defaultâ setting happens to be turned on, too. But, in actuality, regardless of your âApproachable Concurrencyâ setting, the relevant behavior is dictated solely by the separate ânonisolated(nonsending) by defaultâ build setting.
If the ânonisolated(nonsending) by defaultâ build setting is âNoâ, the behavior of async functions is as follows:
syntax
behavior
nonisolated
runs on generic executor
nonisolated(nonsending)
always runs on callerâs actor
@concurrent nonisolated
always runs on generic executor
Bottom line, with ânonisolated(nonsending) by defaultâ option of âNoâ, nonisolated behaves like weâre historically used to, as outlined in SE-0338, where async function marked simply with nonisolated will run on the generic executor (i.e., and therefore will introduce a hop off the current executor).
But, if ânonisolated(nonsending) by defaultâ option is âYesâ (which can happen if you turn on âApproachable Concurrencyâ), then the simple nonisolated changes to now behave like nonisolated(nonsending). As a result, the behavior of async functions with ânonisolated(nonsending) by defaultâ of âYesâ, is now as follows:
syntax
behavior
nonisolated
runs on callerâs actor
nonisolated(nonsending)
always runs on callerâs actor
@concurrent nonisolated
always runs on generic executor
The behavior of a simple nonisolated function changes because you are now using ânonisolated(nonsending) by defaultâ. The end result is that we no longer get that old SE-0338 behavior, and we have to add @concurrent if we really want to get it off the callerâs actor.
From a practical perspective, if you had old asynchronous nonisolated functions that predate Swift 6.2 and you now turn on this ânonisolated(nonsending) by defaultâ build setting for this project, you will have to look at them and figure out whether you want this new behavior (staying on the callerâs actor), in which case no code change is required, or whether you really want them to get off the callerâs actor, in which case youâd add the @concurrent qualifier.
nonisolated functions/properties are not isolated to any actor.
nonisolated is useful inside actors, allowing non-async functions to be called without await.
nonisolated can be used also for asynchronous functions in classes, but there is no use case for these (really?) - these are non-isolated by default (it is a class)
nonisolated(nonsending) is for use in classes (structs?), but makes no sense in actors. nonisolated should be used in actors.
nonisolated(nonsending) means, that the asynchronous function in a class in is not isolated to any actor (it is a class), but even more - it also says that such a function is not sending anything out of the asynchronous context (!) - and this is enforced by using the executor of the caller. So, while nonisolated gives freedom by âany executor can call me, even more executors concurrentlyâ, then (nonsending)limits the async by âok, I am asynchronous, but not as much, I must run on the executor of the callerâ.
Example:
class C {
var b = 0
nonisolated(nonsending)
func classFoo() async {
b = 1
}
}
actor MyActor {
var c = C()
var a = 0
func foo() async {
a = 1
await c.classFoo()
}
}
let t = Task {
let myActor = MyActor()
await myActor.foo()
}
This works (Xcode 26), c is isolated to the actor, and c.classFoo() runs on MyActor executor.
Async function classFoo()must be nonisolated(nonsending), because it says to the actor, that it is safe call this function from an actor.
If function classFoo()were not async, no nonsending is needed.
Async function classFoo()must be nonisolated(nonsending), just nonisolated is not enough.
This example is probably not the greatest example, because c instance cannot exit the actor, so it is concurrency safe, but replacingnonisolated(nonsending)withnonisolated breaks the code.
This example also documents, that nonisolated(nonsending) does NOT guarantee safety, because races on property b in class C happen easily.
If nonisolated will runs on callerâs actor. So I expected my code:
nonisolated
func test() async {
print("test is run on main: \\(Thread.isMainThread)")
}
will print test is run on main: true : because my viewmodel marked with @MainActorMainActor@MainActor final class MyViewModel: ObservableObject
However, ACTUALLY it print test is run on main: false
Please help correct me if any
Thanks
Updated! because I used wrong app scheme when run test.
@hibernat What is your ânonisolated(nonsending) by defaultâ setting? I have to assume it is âNoâ, but wanted to ask before I tackle your questions.
@nhatlee â And what is your ânonisolated(nonsending) by defaultâ build setting? The nonisolated behavior is dependent upon this setting.
For me, with ânonisolated(nonsending) by defaultâ set to âNoâ, when this is called from a function isolated to main actor, this produces the expected legacy behavior, where it runs on the generic executor:
@MainActor
final class Foo {
func test() async {
let bar = Bar()
await bar.test()
}
}
final class Bar {
nonisolated
func test() async {
print("isMainThread: \(Thread.isMainThread)") // â isMainThread: false
}
}
And with ânonisolated(nonsending) by defaultâ set to âYesâ, I get the new behavior, where it runs on the current actor:
final class Bar {
nonisolated
func test() async {
MainActor.assertIsolated()
print("isMainThread: \(Thread.isMainThread)") // â isMainThread: true
}
}
macOS Tahoe 26.0 Beta (25A5338b)
Xcode 26 beta 5 (17A5295f)
Swift Language Mode: âSwift 5â (which is necessary to even use Thread.isMainThread at all)
I tested with settings: Approachable Concurrency : Yes Default Actor Isolation: MainActor Strict Concurrency Checking: Complete Swift Language Version: Swift 6 So I think with those settings above then the nonisolated(nonsending) by default will be YES . Noted: I used my iOS(SwiftUI) project to test, so not sure is it the reason I saw the different behaviour?
Updated after double check my Xcode settings:
Sorry @robert.ryan about the confusion â I mistakenly updated the wrong scheme. I changed the settings for dev but was actually running on prod.
Thanks again for taking the time to help me better understand the new concurrency behavior.
OK, so that means that you are not using this new ânonisolated(nonsending) by defaultâ option from the âSwift Compiler - Upcoming Featuresâ section of an Xcode 26 project build settings. Youâd use a proper Xcode project if you want to enjoy this forthcoming feature. But for the purposes of your original question, Iâll just assume that youâre teasing out the details of the legacy behavior of nonisolated (which is basically equivalent to @concurrent in the world of âApproachable Concurrencyâ).
Yes, in the absence of the new ânonisolated(nonsending) by defaultâ build setting, legacy nonisolated gets it off the current actor isolation and runs it on a generic executor (as outlined in SE-0338). It also means that you cannot access actor isolated state directly.
Yes, if you have a synchronousnonisolated function in an actor, you do not have to await it when calling it (whether from within the actor or externally). Obviously, if it was an async function, youâd always have to await it, whether isolated or not.
Largely correct. If the class is not isolated to some global actor (whether main actor or any global actor), there is little point in declaring a function to be nonisolated.
Iâd be careful, though, because part of the forthcoming âApproachable Concurrencyâ is the option to turn on default isolation to the main actor. [As Holly noted below, this âDefault isolationâ build setting is what dictates the resulting behavior, completely independent of the âApproachable Concurrencyâ build setting.] And for new Xcode 26 projects, this âDefault isolationâ to the main actor is the default setting. So, in the future, if and when you adopt this new convention, it no longer means that a class without some explicit isolation is not isolated. It may be isolated to the main actor if you turn on this build setting. And, obviously, at that point, there may be utility in declaring a function as nonisolated in your class.
But, yes, if the type is already not actor isolated, there is little point in declaring one of its functions as nonisolated. It would be redundant.
I get what youâre going for, but this is a pretty strong assertion. The choice between nonisolated and nonisolated(nonsending), specifically within the context of async functions, is simply:
Use nonisolated (or, in âApproachable Concurrencyâ, @concurrent) if you explicitly want it to hop off the current executor. E.g., maybe youâre doing something that might do something slow and synchronous, like parsing a huge JSON, or saving a file, or converting a big Data into an image, or what have you.
Use nonisolated(nonsending) (or, in âApproachable Concurrencyâ, just nonisolated) if you do not want to introduce this executor hop, presumably because you are not doing anything blocking and you want to avoid the overhead of the executor hop. E.g., perhaps the method only awaits other asynchronous functions and therefore is non-blocking.
Now, in practice, you are correct that one is less likely to use either of these within an actor (because you generally want to enjoy actor isolation). But there are use-cases for this within an actor. Specifically, you might use this for any helper function that doesn't need to access actor isolated state and might be called from outside the actor. E.g., I might have actor for, say, my network API layer, but it includes some helper functions that donât access any actor-isolated state/functions ⌠those might be nonisolated or nonisolated(nonsending); and, again, the choice of which is dictated by whether I need/want to have it hop off the callers current executor or not.
Bottom line, it is not a question of whether it was struct, class, or an actor, but rather what the desired behavior was.
Again, largely correct.
But, I might reframe this: It is not a question of nonisolated(nonsending) âlimitingâ to âthe executor of the callerâ; to my eye it is âenjoying the executor of the callerâ (i.e., avoiding unnecessary executor hops, avoiding headaches around passing non-Sendable objects, etc.).
But if you must hop off the current executor (and you are willing to encumber yourself with Sendable concerns in the process), then, by all means, use nonisolated (a.k.a., @concurrent in the world of âApproachable Concurrencyâ). See SE-0461.
And, again, for the sake of future readers, the Swift 6.2 behavior of nonisolated is dictated by whether you have adopted âApproachable Concurrencyâ (or, technically, the ânonisolated(nonsending) by defaultâ build setting):
ânonisolated(nonsending) by defaultâ
Hop off the callerâs executor
Stay on the callerâs executor
No
nonisolated
nonisolated(nonsending)
Yes
@concurrent
nonisolated
My apologies for belaboring this, but it can be very confusing when nonisolated (by itself) changes meaning (!) based upon a compiler build setting.
Once âApproachable Concurrencyâ is enabled, it enables nonisolated(nonsending) by default, and all (sync+async) functions/accessors will run on the executor of the caller. Only @concurrent async functions get other (generic) executor.
When Default Actor Isolation compiler settings is MainActor (all new projects), then all types get @MainActor annotation. Making real concurrency in âApproachable Concurrencyâ means really a lot of effort to do.
I enabled both âApproachable Concurrencyâ and âDefault Actor Isolationâ, and run this code:
import SwiftUI
class C {
var b = 0
func classFoo() {
b = 1
MainActor.assertIsolated("classFoo is on Main Thread")
sleep(1)
}
}
struct ContentView: View {
var body: some View {
Text("Hello, world!")
.task {
await withTaskGroup(of: Void.self) { group in
MainActor.assertIsolated("Task group is on Main Thread")
let c = C()
group.addTask {
for i in 0..<100 {
await c.classFoo()
print("Task 0: \(i)")
}
}
group.addTask {
for i in 0..<100 {
await c.classFoo()
print("Task 1: \(i)")
}
}
group.addTask {
for i in 0..<100 {
await c.classFoo()
print("Task 2: \(i)")
}
}
}
}
}
}
Class C is now @MainActor by default, so this allows me to share instance of c among 3 concurrent tasks, and call c.classFoo() from each of these tasks. Everything looks and works great, but the tasks do not run concurrently - these are limited by the main actor.
Honestly, I do not think that the âApproachable Concurrencyâ makes things easier. This is definitely more approachable, but it is not any concurrency. It seems to me that it hides a lot of complexity, but concurrency is complex. I am not happy at allâŚ
I hear what youâre saying: That was my reaction when I first heard about âApproachable Concurrencyâ. But in retrospect, theyâve won me over: For many apps, this new paradigm is a great boon. (Personally, I thought that the complexities of writing Swift 6.0 code was just untenable for the platform. Writing code just shouldnât be this hard: I hate to contemplate how many total years of productivity were lost across our entire industry grappling with this!)
No, I think that is whole point: Writing multithreaded code might be complex, but enjoying concurrency doesnât need to be.
The idea behind âApproachable Concurrencyâ is that for many apps, we simply do not need to get into the weeds of writing our own multithreaded code (e.g., all the confusing rules that Swift 6 imposed upon us, such as sendability, region-based-isolation, etc.). Many apps simply donât call for that (beyond, of course, awaiting existing async API, where we are effectively insulated from the complexities of multithreaded implementations).
And, where we really need multithreading (which, for many apps, is infrequently), we can still do that. In those cases, weâre no worse off than we were in Swift 6 (and in many ways, we are better off). But for many apps, the Swift team realized that many apps just donât need to go there at all.
@hibernat â By the way, we may be straying from the original question on this thread, but I did want to tackle your example:
Well, whatever mechanism we use, the updating of b will always need to be updated sequentially, not in parallel. That is how we achieve thread-safety, by never mutating some state variable in parallel.
But before we go there, there is a more serious problem. Given that C is isolated to the main actor, it is essential that we do not do anything that will block that actor. Calling Darwinâs sleep will block that thread, which is a problem if done on the main actor. And if we profile this app with Instruments (adding a âPoints of Interestâ interval around the three addTask calls of the task group), you will see the app is hanging:
Sure, we can remedy this by making C an actor. But more simply, we can just move the calculation into a @concurrent function and be done with it:
class C2 {
var b = 0
func classFoo() async {
b += await simulateSomeComplexCalculation()
MainActor.assertIsolated("classFoo is on Main Thread")
}
/// Simulate some complex calculation.
///
/// Get it off the main actor with `@concurrent` and make it `async`.
///
/// But always returns `1` so we can verify that `b` is being incremented like we want.
@concurrent func simulateSomeComplexCalculation() async -> Int {
let start = ContinuousClock.now
while start.duration(to: .now) < .seconds(1) { }
// return result
return 1
}
}
(I've made two other unrelated changes here, namely not just setting b to 1, but incrementing it by 1. This way we can look at the b at the end and confirm how many times it was successfully updated. I've also replaced sleep with a spinning for a set period of time: Assuming you were trying to simulate a computationally intense calculation, sleep is not a great proxy, because although it blocks the thread, it doesn't keep the CPU busy. Spinning might be a better simulation of a computationally intensive calculation.)
Anyway, when we do that, we can see that it is more performant (doing calculations in parallel), but it also doesn't hang the thread like our original rendition:
Now, my intent is not to dwell on this particular example, but to illustrate that even though we are enjoying the simple synchronization on C offered by âApproachable Concurrencyâ with its default isolation to the main actor, all we need to do is to move the blocking calculation into an async function decorated with @concurrent, and weâre done. Sure, we can take it a step further and make C its own actor, completely getting the whole type off the main actor, if we want, but we donât always have to. It depends upon the details of the business problem you are trying to solve.
My point is that âApproachable Concurrencyâ simplifies the synchronization of C with its default isolation to the main actor. And we donât have to go crazy adding our own synchronization mechanisms: We only need to make sure you never block the main actor.
@hibernat â I might suggest that if you want to continuing teasing this issue out further, maybe we move it to another thread? I hate to distract further from @somuâs original question. Are you good with that?
Just to clarify this point, âApproachable Concurrencyâ and âDefault Actor Isolationâ are two different settings. Approachable Concurrency does not enable main actor by default mode. Both âApproachable Concurrencyâ and âDefault Actor Isolation = MainActorâ are set by default in new app templates created with Xcode 26, but you can change default actor isolation independently from the approachable concurrency upcoming features.