Why don't I need an await in this example?

I have some code like this:

@MainActor
class Test {
  fileprivate var value: Int?
  
  func tryMe() {
    Task {
      self.value = nil
    }
  }
}

This code compiles properly, but my expectation was that, since the Test class is annotated to run only on the main actor, and Task would create a separate task, I would need to write:

await self.value = nil

Not only do I not need that - I get a warning if I try that ("No 'async' expressions occur within 'await' expression").

I think I might be missing something fundamental here. Is the code inside my Task closure still running on the main actor? Is it only Task.detached that will get me a truly separate task?

Yeah, that's exactly right, if you look at the Task initialiser you'll find an annotation called @_inheritActorContext. That means that the closure you pass into the initialiser is isolated the current actor context – and since you annotated your class with @MainActor, that actor context is the main actor. Furthermore, since the annotation is on the whole class, you value property is also isolated to the main actor, which is why you don't need to mark await when accessing it – both the closure inside the Task initialiser and the property are isolated to the main actor.

However, if you look at Task.detached, you'll see that there isn't that @_inheritActorContext annotation, meaning that the closure that you pass into the parameter isn't isolated to the main actor, unlike your value property – which is why you'd need to mark an await when accessing it.

5 Likes