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 awaits and asyncs 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 awaits 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.