Purpose of `Task.immediate`

(Yes I'm familiar with async Swift and how one uses async APIs throughout an application. In a small POC I wanted to simply things by mapping from Async to Sync. Which is not something I would do in production apps.)

Task.immediate was recently shipped and I when I read SE-0472 a couple of months ago I was under the impression that it would allow bridging async code to sync contexts. The title of SE-0472 is Starting tasks synchronously from caller context so I interpreted it as a tool for such bridging.

So I tried writing this helper function:

struct ResultIsNil: Error {}
func bridgeAsyncToSync<T>(operation: @escaping @Sendable () async throws -> T) throws -> T {
	var result: Result<T, Error>?
	Task.immediate {
		do {
			result = await .success(try operation())
		} catch {
			result = .failure(error)
		}
	}
	guard let result else {
		throw ResultIsNil()
	}
	return try result.get()
}

(In a SPM package on macOS 26, in Xcode 26, which requires platforms: [.macOS(.v26)], in Package.swift).

This code does not compile - error: Sending value of non-Sendable type '() async -> ()' risks causing data races since result is mutable.

But if we annotate @MainActor on it and use it like this, this code does compile:

import Testing

struct ResultIsNil: Error {}

@MainActor
func bridgeAsyncToSync<T>(operation: @escaping @Sendable () async throws -> T) throws -> T {
	// same as above, just annotated with @MainActor
}

func get() async throws -> Int {
	try await Task.sleep(for: .seconds(0.5))
	return 1
}

@MainActor
func getSync() throws -> Int {
	try bridgeAsyncToSync {
		try await get()
	}
}

@Test
func testGet() async throws {
	let magic = try await get()
	#expect(magic == 1)
}

@MainActor
@Test
func testGetSync() throws {
	let magic = try getSync()
	#expect(magic == 1)
}

The test testGet() passes, the test testGetSync() does not pass, result is always nil so ResultIsNil is thrown.

So now I'm confused.... Why does the code compile with @MainActor but act in a way such that operation inside Task.immediate never actually gets executed?

So apparently I completely misunderstood Task.immediate..?

But judging by the name and signature of the function, it really looks like it would allow what I wanted to do! And most likely I am not alone in misunderstanding it.

I can’t answer the first part, but I can answer this part:

The key word in the proposal is “starting”. Task.immediate starts synchronously, reaches your sleep, and yields at that point. result obviously hasn’t been set yet. If you added a print (and your main actor kept running after the test completed), you could see that the task does complete eventually, just not in a way that you’d describe as bridgeAsyncToSync.

1 Like

Ok but then to my subject question… what is the purpose?

I see a single - very niche (this not so useful) - purpose: to with ”high priority” (confusingly enough the function also accepts a priority parameter) start a Fire And Forget operation. Because it is impossible to produce a value with this function: I cannot set a variable defined in outer scope, as demonstrated by my failing unit test. Neither can I return a value inside Task, since we cannot call .value on this task since it is an async property

So the name of the function should really be:
Task.immediatelyFireAndForget (and really it shouldn’t take a priority), right?

Hm, per your definition of "fire and forget", you would have to rename Task.detached and Task.init as well. Task.immediate is not different to those other than starting the task synchronously. I'm also not sure, why you think it should not take priority.

2 Likes

Because priority affects when it is run? And the point of this function is to run it immediately?

Not an answer to your question, but your first code sample fails because you forgot to add Sendable constraint to T. The code compiles after adding it. That said, I don't think it should and have just filed #85107.


BTW, I think the purpose of SE-0472 is not to make it possible to bridge async and sync code as in your approach, but to help resolve timing issue between Task code and the code after it.

2 Likes

Priority affects the task as a whole, not just the part which runs until its first suspension point.

The purpose of the API is to help with ordering work within the same isolation. It gives you a guarantee that any synchronous work done at the start of the task will be performed before any other jobs (parts of tasks) that may subsequently be queued on to the same executor.

It is not intended to be an analogue of DispatchQueue.sync.

5 Likes

Note that the task will run until it suspends, which is not necessarily the first await (as it is in your example), and that a task might return a value without suspending.

This means you can do something like this:

@MainActor func fetchMaybeCached() async throws -> Int {
    if Bool.random() {
        print("returning cached value")
        return 42
    } else {
        print("fetching")
        try await Task.sleep(for: .seconds(1))
        print("returning fetched value")
        return 123
    }
}

@MainActor class Fetcher {
    var fetched: Int? { didSet { print("fetched = \(fetched as Any)") } }
    func run() {
        Task.immediate {
            fetched = try await fetchMaybeCached()
        }
        print("started fetch, fetched is \(fetched as Any)")
    }
}

Depending on the coinflip, this will print either of the following when run is called:

returning cached value
fetched = Optional(42)
started fetch, fetched is Optional(42)

fetching
started fetch, fetched is nil
returning fetched value
fetched = Optional(123)

For a real life example, in a UIKit app, you could use this when initializing a UIViewController to have some cached fetch result available immediately when setting up your view, without waiting for the runloop to tick over and potentially having a loading UI show for a single frame.

3 Likes