Skip to content

Suggestion: throws clause and typed catch clause #13219

Description

The typescript type system is helpful in most cases, but it can’t be utilized when handling exceptions.
For example:

function fn(num: number): void {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

The problem here is two fold (without looking through the code):

  1. When using this function there’s no way to know that it might throw an error
  2. It’s not clear what the type(s) of the error is going to be

In many scenarios these aren't really a problem, but knowing whether a function/method might throw an exception can be very useful in different scenarios, especially when using different libraries.

By introducing (optional) checked exception the type system can be utilized for exception handling.
I know that checked exceptions isn't agreed upon (for example Anders Hejlsberg), but by making it optional (and maybe inferred? more later) then it just adds the opportunity to add more information about the code which can help developers, tools and documentation.
It will also allow a better usage of meaningful custom errors for large big projects.

As all javascript runtime errors are of type Error (or extending types such as TypeError) the actual type for a function will always be type | Error.

The grammar is straightforward, a function definition can end with a throws clause followed by a type:

function fn() throws string { ... }
function fn(...) throws string | number { ... }

class MyError extends Error { ... }
function fn(...): Promise<string> throws MyError { ... }

When catching the exceptions the syntax is the same with the ability to declare the type(s) of the error:
catch(e: string | Error) { ... }

Examples:

function fn(num: number): void throws string {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

Here it’s clear that the function can throw an error and that the error will be a string, and so when calling this method the developer (and the compiler/IDE) is aware of it and can handle it better.
So:

fn(0);

// or
try {
    fn(0); 
} catch (e: string) { ... }

Compiles with no errors, but:

try {
    fn(0); 
} catch (e: number) { ... }

Fails to compile because number isn't string.

Control flow and error type inference

try {
    fn(0);
} catch(e) {
    if (typeof e === "string") {
        console.log(e.length);
    } else if (e instanceof Error) {
        console.log(e.message);
    } else if (typeof e === "string") {
        console.log(e * 3); // error: Unreachable code detected
    }

    console.log(e * 3); // error: The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type
}
function fn(num: number): void {
    if (num === 0) {
        throw "error: can't deal with 0";
    }
}

Throws string.

function fn2(num: number) {
    if (num < 0) {
        throw new MyError("can only deal with positives");
    }

    fn(num);
}

Throws MyError | string.
However:

function fn2(num: number) {
    if (num < 0) {
        throw new MyError("can only deal with positives");
    }

    try {
        fn(num);
    } catch(e) {
        if (typeof e === "string") {
           throw new MyError(e);
       } 
    }
}

Throws only MyError.

Activity

  1. changed the title [-]Suggestion: Checked exceptions and typed cache clause[/-] [+]Suggestion: Checked exceptions and typed catch clause[/+] on Dec 29, 2016
  2. DanielRosenwasser commented on Dec 30, 2016

    @DanielRosenwasser
    Member

    Just to clarify - one the ideas here is not to force users to catch the exception, but rather, to better infer the type of a catch clause variable?

  3. nitzantomer commented on Dec 30, 2016

    @nitzantomer
    Author

    Daniel Rosenwasser (@DanielRosenwasser)
    Yes, users won't be forced to catch exceptions, so this is fine with the compiler (at runtime the error is thrown of course):

    function fn() {
        throw "error";
    }
    
    fn();
    
    // and
    try {
        fn();
    } finally {
        // do something here
    }
    

    But it will give developers a way to express which exceptions can be thrown (would be awesome to have that when using other libraries .d.ts files) and then have the compiler type guard the exception types inside the catch clause.

  4. zpdDG4gta8XKpMCd commented on Jan 9, 2017

    @zpdDG4gta8XKpMCd

    how is a checked throw different from Tried<Result, Error>?

    type Tried<Result, Error> = Success<Result> | Failure<Error>;
    interface Success<Result> { kind: 'result', result: Result } 
    interface Failure<Error> { kind: 'failure', error: Error }
    function isSuccess(tried: Tried<Result, Error>): tried is Success<Result> {
       return tried.kind === 'result';
    }
    function mightFail(): Tried<number, string> {
    }
    const tried = mightFail();
    if (isSuccess(tried)) {
        console.log(tried.success);
    }  else {
        console.error(tried.error);
    }

    instead of

    try {
        const result: Result = mightFail();
        console.log(success);
    } catch (error: Error) {
        console.error(error);
    }
  5. added
    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
    and removed on Jan 10, 2017
  6. nitzantomer commented on Jan 10, 2017

    @nitzantomer
    Author

    Aleksey-Bykov

    You're suggesting not to use throw at all in my code and instead wrap the results (in functions that might error).
    This approach has a few drawbacks:

    • This wrapping creates more code
    • It requires that all chain of invoked function return this wrapped value (or error) or alternatively the function that gets Tried<> can not choose to ignore the error.
    • It is not a standard, 3rd party libraries and the native js throw errors

    Adding throws will enable developers who choose to to handle errors from their code, 3rd libraries and native js.
    As the suggestion also requests for error inferring, all generated definition files can include the throws clause.
    It will be very convenient to know what errors a function might throw straight from the definition file instead of the current state where you need to go to the docs, for example to know which error JSON.parse might throw I need to go to the MDN page and read that:

    Throws a SyntaxError exception if the string to parse is not valid JSON

    And this is the good case when the error is documented.

  7. zpdDG4gta8XKpMCd commented on Jan 10, 2017

    @zpdDG4gta8XKpMCd

    And this is the good case when the error is documented.

    is there a reliable way in javascript to tell apart SyntaxError from Error?

    • yes, it's more code, but since a bad situation is represented in an object, it can be passed around to be processed, discarded, stored or transformed into a valid result just like any other value

    • you can ignore tried by returning tried too, tried can be viewed as a monad, look for monadic computations

      function mightFail(): Tried<number, string> {
      }
      function mightFailToo(): Tried<number, string> {
           const tried = mightFail();
           if (isSuccess(tried))  { 
                return successFrom(tried.result * 2);
           } else {
                return tried;
           }
      }
      
    • it's standard enough for your code, when it comes to 3rd party libs throwing an exception it generally means a gameover for you, because it is close to impossible to reliably recover from an exception, reason is that it can be thrown from anywhere inside the code terminating it at an arbitrary position and leaving its internal state incomplete or corrupt

    • there is no support for checked exceptions from JavaScript runtime, and i am afraid it cannot be implemented in typescript alone

    other than that encoding an exception as a special result case is a very common practice in FP world

    whereas splitting a possible outcome into 2 parts:

    • one delivered by the return statement and
    • another delivered by throw

    looks a made up difficulty

    in my opinion, throw is good for failing fast and loud when nothing you can do about it, explicitly coded results are good for anything that implies a bad yet expected situation which you can recover from

  8. zpdDG4gta8XKpMCd commented on Jan 10, 2017

    @zpdDG4gta8XKpMCd

    consider:

    // throw/catch
    declare function doThis(): number throws string;
    declare function doThat(): number throws string;
    function doSomething(): number throws string {
        let oneResult: number | undefined = undefined;
        try {
            oneResult = doThis();
        } catch (e) {
            throw e;
        }
    
        let anotherResult: number | undefined = undefined;
        try {
            anotherResult = doThat();
        } catch (e) {
            throw e;
        }
        return oneResult + anotherResult;
    }
    
    // explicit results
    declare function doThis(): Tried<number, string>;
    declare function doThat(): Tried<number, string>;
    function withBothTried<T, E, R>(one: Tried<T, E>, another: Tried<T, E>, haveBoth: (one: T, another: T) => R): Tried<T, R> {
        return isSuccess(one)
            ? isSuccess(another)
                ? successFrom(haveBoth(one.result, another.result))
                : another
            : one;
    }
    function add(one: number, another: number) { return one + another; }
    function doSomething(): Tried<number, string> {
        return withBothTried(
            doThis(),
            doThat(),
            add
        );
    }
  9. nitzantomer commented on Jan 10, 2017

    @nitzantomer
    Author

    Aleksey-Bykov

    My point with JSON.parse might throwing SyntaxError is that I need to look the function up in the docs just to know that it might throw, and it would be easier to see that in the .d.ts.
    And yes, you can know that it's SyntaxError with using instanceof.

    You can represent the same bad situation with throwing an error.
    You can create your own error class which extends Error and put all of the relevant data that you need in it.
    You're getting the same with less code.

    Sometimes you have a long chain of function invocations and you might want to deal with some of the errors in different levels of the chain.
    It will be pretty annoying to always use wrapped results (monads).
    Not to mention that again, other libraries and native errors might be thrown anyway, so you might end up using both monads and try/catch.

    I disagree with you, in a lot of cases you can recover from thrown errors, and if the language lets you express it better than it will be easier to do so.

    Like with a lot of things in typescript, the lack of support of the feature in javascript isn't an issue.
    This:

    try {
    	mightFail();
    } catch (e: MyError | string) {
    	if (e instanceof MyError) { ... }
    	else if (typeof e === "string") { ... }
    	else {}
    }
    

    Will work as expected in javascript, just without the type annotation.

    Using throw is enough to express what you're saying: if the operation succeeded return the value, otherwise throw an error.
    The user of this function will then decide if he wants to deal with the possible errors or ignore them.
    You can deal with only errors you thrown yourself and ignore the ones which are 3rd party for example.

  10. zpdDG4gta8XKpMCd commented on Jan 10, 2017

    @zpdDG4gta8XKpMCd

    if we talking about browsers instanceof is only good for stuff that originates from the same window/document, try it:

    var child = window.open('about:blank');
    console.log(child.Error === window.Error);

    so when you do:

    try { child.doSomething(); } catch (e) { if (e instanceof SyntaxError) { } }

    you won't catch it

    another problem with exceptions that they might slip into your code from far beyond of where you expect them to happen

    try {
       doSomething(); // <-- uses 3rd party library that by coincidence throws SyntaxError too, but you don' t know it 
    } catch (e) {}
  11. zpdDG4gta8XKpMCd commented on Jan 10, 2017

    @zpdDG4gta8XKpMCd

    besides instanceof is vulnerable to prototype inheritance, so you need to be extra cautions to always check against the final ancestor

    class StandardError {}
    class CustomError extends StandardError {
    }
    function doSomething() { throw new CustomError(); }
    function oldCode() {
       try {
          doSomething();
       } catch (e) {
          if (e instanceof StandardError) {
              // problem
          }
       }
    }
  12. gcnew commented on Jan 10, 2017

    @gcnew
    Contributor

    Aleksey-Bykov Explicitly threading errors as you suggest in monadic structures is quite hard and daunting task. It takes a lot of effort, makes the code hard to understand and requires language support / type-driven emit to be on the edge of being bearable. This is a comment comming from somebody who puts a lot of effort into popularising Haskell and FP as a whole.

    It is a working alternative, especially for enthusiasts (myself included), however I don't think it's a viable option for the larger audience.

  13. 261 remaining items

  14. gastonmorixe commented on Aug 25, 2023

    @gastonmorixe

    Ryan Cavanaugh (@RyanCavanaugh) would it be possible to reopen this please? Thank you

  15. KashubaK commented on Aug 25, 2023

    @KashubaK

    I don't think there's any actionable items in regards to this specific issue at this time. I think the next step should revolve around implementing a solution that encourages error checking in hopes to create widespread adoption of error documentation. After that point I imagine this issue could be brought back up, though I am definitely curious about their thoughts on our recent comments.

    I'm going to try my hand at a linter rule over the weekend. If it goes well, I'll link the repo back here and we can move that conversation there.

  16. gastonmorixe commented on Aug 25, 2023

    @gastonmorixe

    I think it’s important enough to not be closed and encourage more discussion

  17. kvenn commented on Aug 25, 2023

    @kvenn

    Agreed Kyle Kashuba (@KashubaK), I think the TypeScript team has made their opinion known. While I personally don't agree with the decision to deprioritize exception handling, I understand it. They don't think people will adopt it, and so it's on the community to prove otherwise. (That being said, I'd love if they reconsidered :))

    Their reconsideration points made that clear:

    Reconsideration Points
    What would change our evaluation here? Two primary things come to mind:

    1. Widespread adoption and documentation of strong exception hierarchies in JS libraries in the wild
    2. TC39 proposals to implement some sort of pattern-matching criteria to catch exceptions (arguably this would just work "by itself" the same way instanceof works today)

    Kyle Kashuba (@KashubaK), your steps sound right to me. And your idea of integrating with DefinitelyTyped would be a massive step towards adoption (and maybe the only step needed to satisfy reconsideration point 1).

    If you go with the ESLint approach, it looks like eslint-plugin-jsdoc already has the first bullet point (via requires-throw). And can be a good source of inspiration.

    As far as the spec of how exactly it should work

    1. It might be valuable to draft an RFC that covers how it will work (kind of like how it's outlined in the md) - if you're interested in input.
    2. It might be easier to forego having a "safe" throws in the first version (if you're looking to shave off scope). And have a stance on checked vs unchecked exceptions.
      • Swift enforces checked exceptions (you have to specify that you throw and you always have to catch or declare throw)
      • Kotlin has no checked exceptions (you can throw, but there's no enforcement you catch - like TypeScript)
        • They support @throws annotation for interop
      • Java has checked exceptions (with some special unchecked ones)

    Java seems closest to the hybrid of supporting both (safe and unsafe). But I'd personally advocate for matching swift (checked exceptions), but with the ability to opt into

    • None, warning, and error versions of enforcing you declare @throws
    • None, warning, and error versions of enforcing you catch something that declares @throws
  18. KashubaK commented on Aug 26, 2023

    @KashubaK

    Thanks for your input Kyle Venn (@kvenn)!

    The reason I wanted to introduce "safe" exceptions is to address an important note mentioned earlier:

    Constructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch around new RegExp("[A-Z]") since that technically "can" throw an unavoidable exception, even though by inspection it clearly doesn't.

    But perhaps it would be more dangerous and confusing than helpful. Some configuration to whitelist certain functions might be appropriate instead.

    Regarding an RFC, for something like this I wouldn't hesitate, but I have some ideas that I need to make sure are even feasible before I potentially waste others' time. Regardless I would love a community effort, as it will be required to move the needle in any meaningful direction.

  19. KashubaK commented on Aug 26, 2023

    @KashubaK

    I think it’s important enough to not be closed and encourage more discussion

    Personally I agree. That being said, as Kyle Venn (@kvenn) stated, the TS team has already made their stance clear. Robust error handling needs to be normalized in the community before such an invasive new concept is added to countless others' workflows. If our ideas aren't able to proliferate, well, perhaps they were doomed to begin with.

  20. obedm503 commented on Aug 26, 2023

    @obedm503

    Constructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch

    It would be simpler to just use // @ts-expect-error than introducing a whole new error handling concept.

  21. zefir-git commented on Aug 26, 2023

    @zefir-git

    Constructing a RegExp from non-hardcoded input is so vanishingly rare that it seems obnoxious to force a try/catch

    It would be simpler to just use // @ts-expect-error than introducing a whole new error handling concept.

    How would you specify the error type?

  22. obedm503 commented on Aug 27, 2023

    @obedm503

    // @ts-expect-error would allow you to leave "safe" errors unhandled.

  23. KashubaK commented on Aug 28, 2023

    @KashubaK

    // @ts-expect-error would allow you to leave "safe" errors unhandled.

    Yeah, but then you lose all other type-checking for that line.

  24. kvenn commented on Nov 10, 2023

    @kvenn

    Very cool! Any plans to build for IntelliJ?

  25. KashubaK commented on Nov 10, 2023

    @KashubaK

    Michael Angelo Rivera (@michaelangeloio) Super cool, thanks for sharing! I would also love to see a JetBrains plugin implemented.

    Any plans/ideas about a CLI? Could it report the class of error? i.e. class MyCustomError extends Error {}

  26. RyanCavanaugh commented on Nov 10, 2023

    @RyanCavanaugh
    Member

    Locking for clarity. This issue is concluded and will not be reopened; see #13219 (comment) . For general discussion on the topic, continue if needed at #56365.

  27. locked as resolved and limited conversation to collaborators on Nov 10, 2023
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

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions