Pitch: `withIsolation` convenience methods for Actors

Hello Swift Forums,

I have a quick pitch for adding some convenience methods to the Swift standard library as a convenience for a situation that I've encountered a number of times.

I've come across a situation where I would like to run some code that's isolated to a given actor, however, the code that I want to run doesn't necessarily make sense to add to the actor directly. I've accomplished this with the advent of a withIsolation convenience method which I think may be useful to the broader Swift community, hence this pitch.

The method itself is very simple and takes the following form:

extension Actor {

    public nonisolated func withIsolation<R>(_ perform: (isolated Self) async throws -> R) async rethrows -> R {
        try await _withIsolation(perform)

    private func _withIsolation<R>(_ perform: (isolated Self) async throws -> R) async rethrows -> R {
        try await perform(self)

This provides the means of running arbitrary code that is scoped to a given actor that doesn't require that the code be added to the actor directly or via an extension.

Would anyone else find this useful, or useful enough to warrant adding to the standard library directly?


can’t you use isolated parameters for this?

Could you elaborate?

any explanation i could give would likely be inferior to reading the original proposal text for SE-0313.

It looks like Joe is using isolated parameters and would just like some convenient way of running a closure on a specific actor.


but why use isolated on the actor itself, when isolated can be added to the signature of the actor-external function itself?

func f<A>(_ a:A) async where A:Actor
        (a:isolated A) in 

        // closure body goes here
    } (a)
1 Like

That's fair, I think that would work.

1 Like

IMHO add adding such API on "all" actors is going to encourage bad patterns, and that's why I had argued against it way back then (it was suggested as .run back then).

We such API (MainActor.run{}) it specifically on MainActor because yeah it's an actor that you do often just need for "ok, definitely run on this one" and only for reasons of threading. It also is a global actor that tends to sprawl all over an application so it made sense to ease this specific pattern with it.

The problems with offering such API are numerous, primarily:

  1. It makes it harder for people to switch their thinking about actors from "oh it's a queue" and just slap everything into an await actor.run { a in lots of logic } rather than expressing the domain's actual actions the actor is taking as explicit methods "doManyThings()" vs "ok whatever I'll just await actor.run { a.one(); await a.two() } 1)

  2. It makes reentrancy hard to spot and control again -- presumably the "run" would be taking an async closure; so you think you got exclusive access "during the run { ... }" but you don't. If you write your own methods you can make them not async and therefore help yourself to not accidentally cause reentrancy issues (func doManyThings() /* on purpose not async */ )

  3. It also doesn't help the debugging crashes story:
    "oh, we crashed in run, uhm..." versus crashing in exact named methods -- now we got better backtraces but still in release builds you don't always get very precise information i guess still... (and we don't do frame pointers by default on linux I believe so the problem is still a thing).
    Still, having exact method names helps with e.g. profiling your system and you spot "yes, the getThings method is taking a lot of time" vs. "the run() is always taking a lot of time" because too much code was put into the "too easy to use" run {} pattern.

Overall, yes it is "easy" but it is not the best way to use actors and while everyone CAN write this function (call it run, or "with isolated") using a few simple lines of code -- we should not encourage this style as a whole. That's why IMHO it is not a good addition to the standard Actor type.


This makes a hell of a lot of sense. I figured that there was likely a reason that this wasn't implemented in the first place and given this explanation I can certainly agree as to why it wasn't added and why it doesn't make sense to go forward with something like this.