In the following minimal example, the Swift compiler emits a warning Will never be executed pointing to the await in line 2.
func wait(for task: Task<Never, any Error>) async throws -> Never {
try await task.value
}
I think the warning is not correct: Obviously the task can't return normally, but the idea is to wait for (and rethrow) an error.
Is there a better way to write this code? Am I missing something? Or shall I file a bug?
In my real code I have a lot of nested calls to withTaskCancellationHandler and similar with-style functions. They all get the warning at their try await. Anyway, the code works as intended.
I have tested this with Xcode 26.2 as well as the current nightly toolchain.
I am tempted to agree that the warning is incorrect, but Never is kind of special with regards to compiler behavior…
Unless you have a strong motivation to use Never I think I would write the same code simply using Void instead of Never.
That said I think you could get rid of the warning by writing it like this (untested code!)
extension Task where Success == Never {
var waitForError: Never {
get async throws {
guard let error = await self.result.error else {
fatalError("A never task can not return without throwing an error")
}
throw error
}
}
}
func wait(for task: Task<Never, any Error>) async throws -> Never {
return try await task.waitForError
}
What I don’t get here is why waitForError does not show the same “Will never be executed” warning as your original code.
extension Task where Success == Never {
var waitForError: Never {
get async throws {
guard case let .failure(error) = await result else {
fatalError("A never task can not return without throwing an error")
}
throw error
}
}
}
or
extension Result where Success == Never {
consuming func getError() throws -> Never {
if case let .failure(error) = self {
throw error
} else {
fatalError("A never task can not return without throwing an error")
}
}
}
you could get rid of the warning by writing it like this
Thanks for the workaround! I had to adopt the code a bit, as for me Result lacks the error property. Interestingly, this compiles:
extension Task where Success == Never {
var waitForThrow: Never {
get async throws {
switch await result {
case .failure(let error):
throw error
}
}
}
}
The compiler (and me) likes the single case switch
Unless you have a strong motivation to use Never I think I would write the same code simply using Void instead of Never.
I did not want to derail the discussion about the sense of a Task<Never, any Error>, but I think it is a very useful abstraction (and I'm using it a lot). As an example, a task would:
Connect to some service
Setup various protocols
Handle IO
On error: disconnect and tear down all resources belonging to the connection
Whenever the application decides it wants to close the connection, it just cancels the Task. The code in the Task would then throw a CancellationError.
This results in a very nice interface that makes sure that there are no resources on open connections left behind.
So generally: An ongoing operation that has no defined end, but will only terminate on some kind of irregular situation.
Semantically, what’s the advantage of modeling this with Never instead of ()? The type of do { } while someBool doesn’t change just because someBool is statically known to be true.
If you ever want to make it possible to tear down and restart this connection, you’re going to have a heck of a refactoring on your hands.
For me it's not easy to spot the error. But the compiler helpfully emits a warning: "Instance method with uninhabited return type 'Never' is missing call to another never-returning function on all paths". Ah: I need to make the closure -> Never. Otherwise the connection would not be torn down when the closure returns normally.
If you ever want to make it possible to tear down and restart this connection...
This is simple: Just cancel the task that is currently connected. It will disconnect, remove other resources and finally terminate with a CancellationError.
This design has the advantage that the active connection is scoped to the call of withConnection, which stacks very well with Structured Concurrency. It's nice to be sure that, when your function returns, all resources have been torn down. Local Reasoning™️ ftw.
Interesting. I’m not sold on the ergonomics of having to disambiguate any real error from a CancellationError, but I appreciate the novelty of the experiment.