Async no data Race error

Hello :D

I have a question about async and data race error on swift 6.0

when I have this code I have no Error but I am trying to send a noSendable object to async method so I can have data Race ?

struct TestView: View {
 
  var body: some View {
    Rectangle()
      .task {
         let mutableClass = MutableClass()

         for _ in 0 ... 100 {
          Task {
            await Test().nonisolatedMehod(mutableClass: mutableClass)
          }
        }
      }
  }
}

class Test {
  func nonisolatedMehod(mutableClass: MutableClass) async {
     mutableClass.a = 1
     print(mutableClass)
  }
}


class MutableClass {
  var a: Int = 0
}

and here I have an error

struct TestView: View {
  let mutableClass = MutableClass()
  var body: some View {
    Rectangle()
      .task {
        for _ in 0 ... 100 {
          Task {
            print(mutableClass) // run on mainActor
            mutableClass.a = 2 // run on mainActor
            await Test().nonisolatedMehod(mutableClass: mutableClass) // ERROR ending 'self.mutableClass' risks causing data races
          }
        }
      }
  }
}

class Test {
  func nonisolatedMehod(mutableClass: MutableClass) async {
     mutableClass.a = 1
     print(mutableClass)
  }
}


class MutableClass {
  var a: Int = 0
}

I don't undersand why

Am I supposed to be able to pass non-Sendable values to an async nonisolated method?

on this article : What is @concurrent in Swift 6.2? – Donny Wals

we have this sentence

This means that we must make the accessed or passed-in state Sendable, and that can become quite a burden over time. For that reason, making functions nonisolated(nonsending) makes a lot of sense. It runs the function on the caller’s actor (if any) so if we pass state from our call-site into a nonisolated(nonsending) function, that state doesn’t get passed into a new isolation context; we stay in the same context we started out from. This means less concurrency, and less complexity in our code.

so I am not sure to understand

I use swift 6.0 with strict concurency checking to complete

Because here I am update ma var on background multiple background thread I suppose to have dataRace ?

Hey there! Author of the article you linked to here; let me start by stating that the article you linked doesn't seem to be fully related to the issue at hand since the article is about Swift 6.2's @concurrent and nonisolated(nonsending) and you're not using 6.2 it looks like.

In any case, the problem you're seeing is quite interesting because in the first case you're allowed to pass your main actor isolated class into a nonisolated function, but in the second case you're not allowed to do this.

I think the difference here is all in the way things are scoped and there's an interplay of region based isolation and the sending keyword.

You can read up on these topics here:

In essence, I think the compiler is able to prove that mutableClass is only ever accessed by one task at a time. You create it from one task, and then pass it into a bunch of other tasks that run on the main actor. Since everything is on main, and mutableClass is scoped to that view modifier task and never passed to a place outside of that view modifier task, everything is fine.

In the second example, the mutableClass is scoped to your view which means it's not scoped to the task you created in the view modfier. The compiler isn't able to prove that there's never any concurrent access possible because due to the extra layer involved in the second example.

Passing non-sendable state across isolation boundaries is only allowed if the compiler can prove that it's safe (through region based isolation and sending)

What's interesting here is that you'd think the compiler can prove this but I guess just beyond the compiler's capabilities (at this point)

1 Like

I think the reason for the different behavior is that

  • mutableClass in example 1 is a local variable in disconnected region, so it can be sent to other isolation domain.
  • mutableClass in example 2 is a property in MainActor isolated struct, so it's in actor isolated region and can't be sent.

That said, I think example 1 should fail to compile because a single non-sendable value shouldn't be sent to multiple tasks (once a non-sendable value is sent to a task, it's isolated to that task). Which Swift version are you using? I have verified this example on Swift 6.1.2, which is a non-SwiftUI version of your example 1. It fails as I expected. Removing the for-loop fixes it.

class MutableClass {
    var a: Int = 0
}

nonisolated func nonisolatedMehod(mutableClass: MutableClass) async {
    mutableClass.a = 1
    print(mutableClass)
}

func test() {
    let mutableClass = MutableClass()

    for _ in 0 ... 100 {
        Task {
            await nonisolatedMehod(mutableClass: mutableClass)
        }
    }
}

PS: I've moved the thread to "using swift" forum.

Hello Thank for your answer and also for your article What is @concurrent in Swift 6.2? – Donny Wals

it was really interesting :D

I actually referenced your article because I wasn't sure I fully understood the concept of Sendable, and to me, the logic seemed more in line with what you described in the article rather than what I was seeing in Xcode.

for me we should always have sendable code in async method to be sure to have no issue

hello and thank you for your answer

I am on swift 6.0 maybe it's why It's running because I try your code and it's running as well

I am updating my xcode to check do I need to update something else than

strict concurency checking to complete ? :)

I haven't used Xcode for quite a while so I'm not sure. IIRC if you create a new project in latest version of Xcode, it uses Swift 6 language mode and hence strict concurrency checking by default.

But there is another way to do a quick check: using Swift command in terminal. That's what I use. For example, to verify above code, I first create a swift package:

$ swift package init --type executable

Then use swift build or swift run to verify if it compiles.

If I run this code without a @MainActor, it behave as expected, it won't run : because task take sending closure and we are using mutableClass in multiple scope

func test() {
    let mutableClass = MutableClass()

    for _ in 0 ... 100 {
        Task {
            await nonisolatedMethod(mutableClass: mutableClass)
        }
    }
}

This doesn't work, because we're trying to mutate a non-Sendable object (mutableClass) from multiple concurrent tasks. and sending prevents us from doing that

So far, this makes sense.

Here’s the simpler version that does work :

func test() {
    let mutableClass = MutableClass()

    Task {
        await nonisolatedMethod(mutableClass: mutableClass)
    }
}

This one makes sense, because it's only a single task modifying the object — so there's no violation of isolation rules. and because we have sending closure on task, so we can use noSendable class in one scope

But when I add @MainActor to the function like this:

@MainActor
func test() {
    let mutableClass = MutableClass()

    for _ in 0 ... 100 {
        Task {
            await nonisolatedMethod(mutableClass: mutableClass)
        }
    }
}

It runs. All the tasks execute without compiler errors. Since we're on the MainActor, the Task inherits the actor context, so it has no issue accessing mutableClass and passing it to nonisolatedMethod. We're essentially using MutableClass on the same actor.

However, this is a bit confusing to me. The mutableClass instance is being passed into a non-mutating function (nonisolatedMethod), but since the function is being invoked across multiple concurrent tasks, possibly on background threads, I expected a similar safety violation.

So why is this allowed? nonisolatedMethod is still being called concurrently across multiple tasks, and MutableClass is not Sendable. That seems like it should be unsafe.


I think your concern is valid. It appears a bug to me, because a non-sendable value shouldn't be sent to multiple tasks no matter what isolation test() is. I'm able to reproduce the issue in Swift-6.2-snapshot-2025-06-17. Could you file a bug?

BTW, I filed bug #82230 a while back. Its code isn't exactly same as yours. But both are common in that compiler can detect an issue successfully if the test function is nonisolated but can't if it's @MainActor (in my case the compiler crashes if the test function is nonisolated, but it fails to compile anyway :)

thank you for you feedback I will do it :D

I am still learning swift concurrency so my comment could be completely wrong.

Changes to your 1st code block

  • Just to eliminate any ambiguity of Default Actor Isolation, I have explicitly annotated.
  • I have also replaced Task with Task.detached
  • Then the error Sending value of non-Sendable type '() async -> ()' risks causing data races is shown.
struct TestView: View {
 
  var body: some View {
    Rectangle()
      .task {
         let mutableClass = MutableClass()

         for _ in 0 ... 100 {
         Task.detached {
            await Test().nonisolatedMehod(mutableClass: mutableClass) // Sending value of non-Sendable type '() async -> ()' risks causing data races
          }
        }
      }
  }
}

nonisolated class Test {
  func nonisolatedMehod(mutableClass: MutableClass) async {
     mutableClass.a = 1
     print(mutableClass)
  }
}

nonisolated class MutableClass {
  var a: Int = 0
}

My inference (could be completely wrong):

  • Reason why Task compiled ok is because Task could be inheriting the context and therefore runs on the main thread
  • Reason why Task.detached threw compilation error is because it doesn't inherit context and therefore is not mandated to run on the main thread.

yes because here Task.detached { won't inherit from task so it's not on the mainActor

this part will work because you are on different actor but you create mutableClass in sending scope so you have no issue

.task {
         let mutableClass = MutableClass()
         Task.detached {
            await Test().nonisolatedMehod(mutableClass: mutableClass) // Sending value of non-Sendable type '() async -> ()' risks causing data races
          
        }
      }

but when we add the for we are trying to send our mutableClass in multiscope and it's not possible

  • Reason why Task compiled ok is because Task could be inheriting the context and runs on the main thread : yes agree with that but I don't understand why we are allow to send the mutableClass to nonisolatedMehod :'(

@NicolasL Could you check if the Default Actor Isolation is set to Main Actor?

If so then:

  • It would mean that every class if not explicitly stated would be implicitly @MainActor would be safe to be accessed
  • Test and MutableClass would be main actor implicitly
  • Your nonisolatedMehod would be main actor as well unless explicitly stated as nonisolated

Following would throw an error:

@MainActor class Test {
  nonisolated func nonisolatedMehod(mutableClass: MutableClass) async {
     mutableClass.a = 1 // Main actor-isolated property 'a' can not be mutated from a nonisolated context
     print(mutableClass)
  }
}

@MainActor class MutableClass {
  var a: Int = 0
}

I am sorry I am not to sure to undersand :'(

If I have understand here we have class test as MainActor context but as we have nonisolatedMehod as nonisolated this code will be call on background thread

 mutableClass.a = 1 // Main actor-isolated property 'a' can not be mutated from a nonisolated context
     print(mutableClass)

since we have MutableClass on MainActor and nonisolatedMehod is nonisolated we are not able to update mutableClass.a because we cannot update isolated class from a nonisolated
async context

for me your exemple is logic

I am also new so I could be wrong as well.

I found the following videos helpful

1 Like