28

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.

nikeee
  • 10,248
  • 7
  • 40
  • 66
  • 4
    I think it is a bug. await for Promise, IMO should make the rest of the code unreachable as well. I suggest creating a bug for typescript – Haim Houri Nov 06 '19 at 15:21
  • Mabe it is intentional due to backwards compability. – nikeee Nov 06 '19 at 15:28
  • 7
    I opened an issue https://github.com/microsoft/TypeScript/issues/34955 – nikeee Nov 07 '19 at 00:39
  • 4
    It is now considered a bug. – nikeee Dec 06 '19 at 20:18
  • I was tracking it too man, was rooting for you hahaha – Haim Houri Dec 09 '19 at 16:59
  • I think the code is unreachable because an error is thrown. However, as far as I know, TypeScript has no way of knowing that an error will be thrown, since it has no `throws` functionality, unlike e.g. in Java, which defines what types of errors can be thrown by the method. And if an error is not thrown, then the code can simply continue and the `a0` type will simply be `never` and that doesn't prevent the compiled JavaScript from reaching the code after it. If you disregard the throwing of an error, this code is perfectly runnable in vanilla JS. – undefined Mar 04 '23 at 08:06
  • 3
    @undefined A value can't simply have type `never` as there are no values of that type. The whole point of `never` is to represent non-termination, which is why in the version of OP's code without promises the code after the call is correctly recognized as unreachable. "If you disregard the throwing of an error, this code is perfectly runnable in vanilla JS." The throwing of the exception (or some other form of non-termination) is required for the return type to be declared as `never`. – sepp2k Mar 04 '23 at 09:20
  • @sepp2k `const a = (): never => void 0 as never;` is valid TS that returns `never`. The code is unreachable in the case of `Promise.reject()` because an error is thrown (in other cases it could be because of an endless loop). However, TypeScript has no way of knowing that methods can throw errors currently. I think if it could, then the error would be due to unhandled exception rather than unreachable code. And if you have an unhandled exception, then it is clear that the execution terminates and any following code would be unreachable. – undefined Mar 05 '23 at 14:15
  • 3
    @undefined That type assertion is simply wrong. That function does not actually return `never` but rather `undefined`, so declaring it as returning `never` is incorrect. TypeScript doesn't prevent you from shooting yourself in the foot with type assertions. – Bergi Mar 05 '23 at 18:49
  • @Bergi the code is not wrong, it's perfectly valid, and as far as TS is concerned, `never` is the return type. This code compiles without error, returns `never` (according to TS) and doesn't prevent subsequent code from executing (because that actually depends on the underlying JS to which the returned value is `undefined` and no cause to terminate). Given this, I don't understand why a TypeScript type should determine whether subsequent code is reachable. On the other hand, declaring what errors a method throws is something already supported in Java, C++, Swift and other. – undefined Mar 05 '23 at 19:27
  • 4
    @undefined TypeScript accepting some code without compiler errors does not mean that it isn't wrong. It also doesn't complain about `() => 42 as unknown as string`, `() => void 0 as never as {}[]` or `() => null as unknown as object`, which still are clear programmer mistakes. – Bergi Mar 05 '23 at 19:44

2 Answers2

1

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 2023, the issue is still unresolved.

kaya3
  • 47,440
  • 4
  • 68
  • 97
-1

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.

Atul Rajput
  • 4,073
  • 2
  • 12
  • 24
  • 3
    "*Because there is a chance that an error will be thrown during runtime, the code after the await is considered as reachable.*" - that doesn't make sense. The point is that the "chance" is 100%, and there is in fact no chance that no exception will be raised, so the code after the `await` should be considered unreachable. – Bergi Jun 20 '23 at 06:34
  • 1
    No point in answering this question, it already has been classified as a bug in the TS compiler – nikeee Jun 22 '23 at 12:16