Reputation: 10724
Assume we have this function:
function returnNever(): never {
throw new Error();
}
When creating an IIFE, the code that comes after it becomes marked as unreachable:
(async () => {
let b: string;
let a0 = returnNever();
b = ""; // Unreachable
b.toUpperCase(); // Unreachable
})();
This works as expected. Note that a0
is inferred to be of type never
.
However, if returnNever()
returns a Promise<never>
and gets awaited, the behaviour is different:
(async () => {
let b: string;
let a1 = await Promise.reject(); // returns Promise<never>
b = ""; // Not unreachable?
b.toUpperCase(); // Not unreachable?
})();
In this case, a1
is also inferred to be of type never
. But the code afterwards is not marked as unreachable. Why?
Background:
I recently stumbled upon some logError
function that looked like in the following code. It was used inside a catch
block. This way, I discovered, that not reachability analysis, but also definite assignment analysis is influenced by that:
declare function fetchB(): Promise<string>;
async function logError(err: any): Promise<never> {
await fetch("/foo/...");
throw new Error(err);
}
(async () => {
let b: string;
try {
b = await fetchB(); // Promise<string>
} catch (err) {
await logError(err); // awaiting Promise<never>
}
b.toUpperCase(); // Error: "b" is used before assignment
})();
If logError
is made synchronous (by removing all await
s and async
s that have to do with logError
), there is no error. Also, if let b: string
is changed to let b: string | undefined
, the undefined
is not getting removed after the try-catch block.
It seems that there is a reason to not consider await
s of Promise<never>
-returning functions in any aspect of the control flow analysis.
It might also be a bug, but I rather think that I am missing some detail here.
Upvotes: 29
Views: 1140
Reputation: 51142
It might also be a bug, but I rather think that I am missing some detail here.
It is indeed a bug, and the GitHub issue (reported by the author of this question) regarding it can be found here. As of January 2025, the issue is still unresolved.
Upvotes: 2
Reputation: 4178
A **Promise<never>**
is treated as a type that might throw any error, including runtime errors, when you await it in TypeScript. Because there is a chance that an error will be thrown during runtime, the code after the await is considered as reachable.
In order to prepare for unexpected runtime mistakes, TypeScript's behaviour with Promise<never>
types is intended to be more liberal & conservative. Although being at times illogical, it is a deliberate choice made to address the uncertainty brought on by promises and potential runtime exceptions while maintaining the soundness of the type system.
Upvotes: -1