Be careful wrapping a throwing function in a Task
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
Advertisement
RevenueCat’s State of Subscription Apps report is out.
Built on data from 115,000+ apps generating over $16B in revenue, it gives developers real benchmarks for pricing, conversion, trials, and retention so you can see how your app compares.
Sponsors like RevenueCat really help me grow my content creation, so if you have time please make sure to have a look at what they offer: it’s a direct support to my content creation 🙂
Can you guess what’s wrong with this code?
We have an asynchronous throwing function and for the purpose of this example it will always throw an Error:
And then we have a synchronous throwing function that calls this asynchronous function through the use of a Task:
At first glance, this code looks perfectly fine.
However, it has one very tricky pitfall!
Let’s say that we call the function loginUser() inside a do block, in order to catch and handle the Error:
If we run this code, we’ll notice that, even though the Error is thrown, the code in the catch block never gets executed.
This might seem strange but this is actually a totally normal and intended behavior!
What’s happening is that the Error is indeed thrown, but it is caught by the Task.
And from that point, the Task has no way of re-throwing the Error to the function loginUser(), because the Task is running asynchronously and the function loginUser() has already finished executing!
And actually the reason why this confusion was made possible is because I’ve made a mistake when writing this code!
You see, I’ve declared the function loginUser() as throwing, but there’s actually no call to a throwing function inside its scope.
Using the keyword throws here is completely superfluous, and if I remove it we can see that the code still builds, but now we get a very clear warning that the catch block will never be executed:
I’ve myself seen this mistake find its way in a codebase, so I would definitely recommend that you check that you also haven’t made it yourself!
Here’s the code if you want to experiment with it:
import Foundation
enum AuthenticationError: Error {
case invalidCredentials
}
func verifyUserCredentials() async throws {
throw AuthenticationError.invalidCredentials
}
func loginUser() {
Task {
try await verifyUserCredentials()
}
}
do {
try loginUser()
} catch {
print("Authentication failed: \(error)")
}