Skip to content

TypeScript does not recognize unreachable code after a method returning never is called outside of its defining scope #59138

Open

Description

@dkfdkdodkms

Does this issue occur when all extensions are disabled?: Yes

  • VS Code Version: 1.89.1
  • OS Version: All

To Reproduce:

class SimpleThrower {
    public throwError(): never {
        throw new Error('');
    }

    public doSomething(): string {
        this.throwError(); // Directly call the never-returning function
        
        return 'Success'; // TypeScript recognizes that this line is unreachable
    }
}

() => {
    const example = new SimpleThrower();
    console.log(example.doSomething()); // This will throw an error
    example.throwError(); // Throws an error and stops execution
    console.log('foobar'); // TypeScript does not recognize that this line is unreachable
}

Playground Link

Expected behavior:

TypeScript should recognize that the line console.log('foobar'); is unreachable after example.throwError(); is called, similar to how it recognizes the unreachable code within the doSomething method.

Actual behavior:

TypeScript does not mark the line console.log('foobar'); as unreachable after the example.throwError(); call in the global scope or within a different function scope.

Activity

  1. removed their assignment
    on Jul 3, 2024
  2. SamVerschueren commented on Aug 22, 2024

    @SamVerschueren

    I ran into this exact same issue today. (See playground)

    const process = {
      foo(): never {
        throw new Error('bar');
      },
    } as const;
    
    function foo(): never {
      throw new Error('foo');
    }
    
    process.foo();
    
    console.log('hello');
    
    foo();
    
    // from this point on, the unreachable code is detected
    
    console.log('world');

    However, if I use an interface to define process, it does work correctly. (See playground)

    interface IProcess {
      foo(): never;
    }
    
    const process: IProcess = {
      foo(): never {
        throw new Error('bar');
      },
    } as const;
    
    function foo(): never {
      throw new Error('foo');
    }
    
    process.foo();
    
    // from this point on, the unreachable code is detected
    
    console.log('hello');
    
    foo();
    
    console.log('world');

    But then again, it doesn't work if you use [key: string] as the key in the interface. (See playground)

    interface IProcess {
      [key: string]: () => never;
    }
    
    const process: IProcess = {
      foo(): never {
        throw new Error('bar');
      },
    } as const;
    
    function foo(): never {
      throw new Error('foo');
    }
    
    process.foo();
    
    console.log('hello');
    
    foo();
    
    // from this point on, the unreachable code is detected
    
    console.log('world');
  3. amannn commented on Oct 18, 2024

    @amannn

    I ran into this problem too:

    function getThrowError() {
      return function throwError() {
        throw new Error();
      };
    }
    
    const throwError = getThrowError();
    
    throwError();
    
    // Should be unreachable
    console.log("test");

    Playground

    I couldn't get it to work with an interface either.

  4. DScheglov commented on Oct 22, 2024

    @DScheglov

    Actually I think it is a bug, it is not a just suggestion.

    interface Throwable {
        throw(): never;
    }
    
    function fn(value: string | Throwable): string {
        if (typeof value !== "string") value.throw();
        return value;
    //  ^^^^^^ - Type 'string | Throwable' is not assignable to type 'string'.
    //           Type 'Throwable' is not assignable to type 'string'.
    }

    Playground

    But this code works well:

    interface Throwable {
        throw(): never;
    }
    
    declare function fail(): never;
    
    function fn(value: string | Throwable): string {
        if (typeof value !== "string") fail();
        return value;
    }

    Playground

    As well as the following one:

    declare const error: {
      throw(): never;
    }
    
    declare const value: string | undefined;
    
    if (value == null) error.throw();
    const result: string = value;

    Playground

    This code doesn't work again:

    interface Throwable {
        throw(): never;
    }
    
    declare function fail(): never;
    
    const context = { fail };
    
    function fn(value: string | Throwable): string {
        if (typeof value !== "string") context.fail();
        return value;
    //  ^^^^^^ - Type 'string | Throwable' is not assignable to type 'string'.
    //           Type 'Throwable' is not assignable to type 'string'.
    }

    Playground

    So, currently we have the patch with adding return before throw():

    function fn(value: string | Throwable): string {
        if (typeof value !== "string") return value.throw();
        return value;
    }

    Playground

    But it is definitelly better to fix the inconsistent behaviour.

    Ryan Cavanaugh (@RyanCavanaugh), could you please help with raising the priority of this issue?

  5. alecmev commented on Jul 21, 2025

    @alecmev

    Duplicate of #60368, I think?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions