Hi Swifters 🙂, in this article, I would like to talk about withTaskGroup and withThrowingTaskGroup in Swift 6.1. If you have ever worked with concurrency in Swift, you might have probably seen or used withTaskGroup
or withThrowingTaskGroup
, if not, thats okay, I will tell you why and when you should consider using it. These APIs let you spin off multiple concurrent tasks and then wait for them to finish, which is awesome for making your app more efficient.
Ok but Wait, What Even Is Concurrency? async
, and await
?
Before we dive into this improvement in Swift 6.1, let’s take a step back and cover some basics. So what is Concurrency? Concurrency lets your app do more than one thing at the same time, like fetch data from multiple places without freezing the screen.
async
marks a function that runs asynchronously, it might suspend and come back later.await
lets you pause and wait for thatasync
function to give you a result.
Swift does the heavy lifting behind the scenes, so your app stays smooth and responsive. Here goes a quick example:
func fetchData() async -> String {
return "Data fetched"
}
func run() async {
let result = await fetchData()
print(result)
}
I hope that made sense!
Now moving towards the withTaskGroup
in Swift 6.1
Let’s say you want to do the same async thing multiple times, like downloading a bunch of images. Instead of writing repeated code or manually managing tasks, Swift gives you withTaskGroup
. This is basically a handy function that lets you add multiple tasks to a group, run them all at once, and then collect the results neatly. It also manages the task lifecycle for you (which means no memory leaks, yayyy!) and handles cancellation if needed. Before Swift 6.1 you always had to write the child task result type as an argument when creating the task group. For example, here is how you would use withTaskGroup
before Swift 6.1.
func fetchNumbers() async -> [Int] {
await withTaskGroup(of: Int.self) { group in
for i in 1...3 {
group.addTask {
return i * 2
}
}
var results = [Int]()
for await value in group {
results.append(value)
}
return results
}
}
Notice “of: Int.self
?” You had to tell the compiler what each task was going to return. But after Swift 6.1 you can safely ignore this part. Here is how you can write the same code now.
func fetchNumbers() async -> [Int] {
await withTaskGroup { group in
for i in 1...3 {
group.addTask {
return i * 2
}
}
var results = [Int]()
for await value in group {
results.append(value)
}
return results
}
}
No more “of: Int.self
” the compiler looks at your addTask
closures and figures out what type they return. Seems good right?
Ok Now Why Should You Care?
If your task returns a complex type, like a tuple or a custom struct, having to specify it manually is annoying and error-prone. And if you change the return type later, you have to update it everywhere that sounds annoying too. Swift 6.1 basically removes that friction. The compiler just knows. Your future self will thank you.
But What If One of My Tasks Can Fail?
That’s where withThrowingTaskGroup
comes in. It’s just like withTaskGroup
, but it lets your tasks throw errors. Why’s that useful? Imagine you’re fetching data from a bunch of APIs and one of them fails. With a throwing task group, you can catch and handle that failure gracefully. Here goes an example efore Swift 6.1.
func fetchWithThrowingGroup() async throws -> [String] {
try await withThrowingTaskGroup(of: String.self) { group in
group.addTask {
return "Hello"
}
group.addTask {
return "World"
}
var results = [String]()
for try await value in group {
results.append(value)
}
return results
}
}
Here goes the same code After Swift 6.1.
func fetchWithThrowingGroup() async throws -> [String] {
try await withThrowingTaskGroup { group in
group.addTask {
return "Hello"
}
group.addTask {
return "World"
}
var results = [String]()
for try await value in group {
results.append(value)
}
return results
}
}
Same pattern, same cleaner syntax.
This might seem like a small thing, but it’s a huge win for code clarity and developer happiness. The compiler doing more work = you doing less. If you’re using Swift’s concurrency features, you’ll definitely appreciate this subtle yet powerful update. Less noise, fewer bugs, and a smoother ride overall.
I hope you enjoyed the article. 🙂 HAPPY LEARNING.
For further details about this concept, feel free to visit the official documentation at:
https://developer.apple.com/documentation/swift/withtaskgroup(of:returning:isolation:body:)
https://developer.apple.com/documentation/swift/withthrowingtaskgroup(of:returning:isolation:body:)