nikeee
nikeee

Reputation: 10724

Why is Promise<never> not considered in reachability analysis?

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.

Upvotes: 29

Views: 1140

Answers (2)

kaya3
kaya3

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

Atul Rajput
Atul Rajput

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

Related Questions