Task(priority: .background) can still run on the main actor. Priority doesn’t select a thread. I got that distinction wrong in the original version of this note.
Updated 7 October 2026. Examples checked with Swift 6.4 in Swift 6 language mode. The fetch functions below are placeholders for your own asynchronous APIs.
Tasks, actors and threads
Task {} creates an unstructured task, not a child task. Its closure inherits the surrounding actor isolation. Inside a @MainActor function, changing the priority doesn’t remove that isolation:
@MainActor
func startWork() -> Task<Void, Never> {
Task(priority: .background) {
MainActor.assertIsolated()
}
}
Task.detached is also unstructured, but doesn’t inherit the caller’s actor isolation, priority or task-local values. It isn’t a promise of a dedicated background thread, and calling an actor-isolated function can still hop to that actor.
Use async let or a task group when the work should be a child task with a lifetime bounded by the enclosing scope. With either form of unstructured task, you manage its lifetime and cancellation yourself.
How to run concurrently
Using async let, you can start a fixed number of child tasks concurrently. That allows their work to overlap; it doesn’t guarantee simultaneous execution on different threads.
async let listing = fetchListing()
async let favorites = fetchFavorites()
// Then await for both
let all = await (listing, favorites)
How to run concurrently with TaskGroup
TaskGroup lets you create a number of child tasks determined at runtime. The group waits for its children before leaving its scope.
// This task group will return the UIImage types
try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in urls {
group.addTask {
return try await fetchImage(from: url)
}
}
for try await image in group {
// Handle as each image is fetched
}
}
If you only need to wait for completion, use Void and discard each result. Keep try await so a fetch failure reaches the caller:
try await withThrowingTaskGroup(of: Void.self) { group in
for url in urls {
group.addTask {
_ = try await fetchImage(from: url)
}
}
try await group.waitForAll()
}
How to limit the number of concurrent running tasks
Let’s say you want to fetch at most 5 images at a time. Use next() to collect a completed result before adding another task, then collect the remaining results. The images arrive in completion order, not URL order.
let images = try await withThrowingTaskGroup(of: UIImage.self) { group in
var images: [UIImage] = []
for (i, url) in urls.enumerated() {
if i >= 5, let image = try await group.next() {
images.append(image)
}
group.addTask {
return try await fetchImage(from: url)
}
}
for try await image in group {
images.append(image)
}
return images
}
Cancellation
Task groups use cooperative cancellation. Your work must respond by checking Task.isCancelled, calling try Task.checkCancellation(), or using an API that already responds to cancellation.
addTaskUnlessCancelled only avoids adding a child when the group is already cancelled. A child that has started still needs to cooperate with later cancellation.