Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Suggestion: throws clause and typed catch clause #13219

Open
nitzantomer opened this issue Dec 29, 2016 · 222 comments · Fixed by microsoft/workspace-tools#86
Open

Suggestion: throws clause and typed catch clause #13219

nitzantomer opened this issue Dec 29, 2016 · 222 comments · Fixed by microsoft/workspace-tools#86
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@nitzantomer
Copy link

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.

@DanielRosenwasser DanielRosenwasser changed the title Suggestion: Checked exceptions and typed cache clause Suggestion: Checked exceptions and typed catch clause Dec 29, 2016
@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Dec 30, 2016
@DanielRosenwasser
Copy link
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?

@DanielRosenwasser DanielRosenwasser added the In Discussion Not yet reached consensus label Dec 30, 2016
@nitzantomer
Copy link
Author

nitzantomer commented Dec 30, 2016

@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.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 9, 2017

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);
}

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature and removed In Discussion Not yet reached consensus labels Jan 10, 2017
@nitzantomer
Copy link
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.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 10, 2017

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

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 10, 2017

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
    );
}

@nitzantomer
Copy link
Author

nitzantomer commented Jan 10, 2017

@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.

@zpdDG4gta8XKpMCd
Copy link

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) {}

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 10, 2017

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
      }
   }
}

@gcnew
Copy link
Contributor

gcnew commented Jan 10, 2017

@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.

@aluanhaddad
Copy link
Contributor

Actually, my main concern here is that people will start subclassing Error. I think this is a terrible pattern. More generally, anything that promotes the use of the instanceof operator is just going to create additional confusion around classes.

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 10, 2017

This is a comment comming from somebody who puts a lot of effort into popularising Haskell and FP as a whole.

i really think this should be pushed harder to the audience, not until it's digested and asked for more can we have better FP support in the language

and it's not as daunting as you think, provided all combinators are written already, just use them to build a data flow, like we do in our project, but i agree that TS could have supported it better: #2319

@gcnew
Copy link
Contributor

gcnew commented Jan 10, 2017

Monad transformers are a real PITA. You need lifting, hoisting and selective running fairly often. The end result is hardly comprehendible code and much higher than needed barrier of entry. All the combinators and lifting functions (which provide the obligatory boxing/unboxing) are just noise distracting you from the problem at hand. I do believe that being explicit about state, effects, etc is a good thing, but I don't think we have found a convenient wrapping / abstraction yet. Until we find it, supporting traditional programming patterns seems like the way to go without stopping to experiment and explore in the mean time.

PS: I think we need more than custom operators. Higher Kinded Types and some sort of type classes are also essential for a practical monadic library. Among them I'd rate HKT first and type classes a close second. With all that said, I believe TypeScript is not the language for practicing such concepts. Toying around - yes, but its philosophy and roots are fundamentally distant for a proper seamless integration.

@gcnew
Copy link
Contributor

gcnew commented Jan 10, 2017

Back to the OP question - instanceof is a dangerous operator to use. However explicit exceptions are not limited to Error. You can throw your own ADTs or custom POJO errors as well. The proposed feature can be quite useful and, of course, can also be misused pretty hard. In any case it makes functions more transparent which is undoubtedly a good thing. As a whole I'm 50/50 on it :)

@nitzantomer
Copy link
Author

@Aleksey-Bykov

Developers should be aware of the different js issues you described, after all adding throws to typescript doesn't introduce anything new to js, it only gives typescript as a language the ability to express an existing js behavior.

The fact that 3rd party libraries ca throw errors is exactly my point.
If their definition files were to include that then I will have a way to know it.

@aluanhaddad
Why is it a terrible pattern to extend Error?

@gcnew
As for instanceof, that was just an example, I can always throw regular objects which have different types and then use type guards to differentiate between them.
It will be up to the developer to decide what type of errors he wishes to throw, and it probably is the case already, but currently there's no way to express that, which is what this suggestion wants to solve.

@gcnew
Copy link
Contributor

gcnew commented Jan 10, 2017

@nitzantomer Subclassing native classes (Error, Array, RegExp, etc) was not supported in older ECMAScript versions (prior to ES6). The down level emit for these classes gives unexpected results (best effort is made but this is as far as one can go) and is the reason for numerous issues logged on daily basis. As a rule of thumb - don't subclass natives unless you are targeting recent ECMAScript versions and really know what you are doing.

@nitzantomer
Copy link
Author

@gcnew
Oh, I'm well aware of that as I spent more than a few hours trying to figure out what went wrong.
But with the ability to do so now there shouldn't be a reason not to (when targeting es6).

In anycase this suggestion doesn't assume that the user is subclassing the Error class, it was just an example.

@gcnew
Copy link
Contributor

gcnew commented Jan 11, 2017

@nitzantomer I'm not arguing that the suggestion is limited to Error. I just explained why it's a bad pattern to subclass it. In my post I actually defended the stance that custom objects or discriminated unions may be used as well.

instanceof is dangerous and considered an anti-pattern even if you take out the specificities of JavaScript - e.g. Beware of instanceof operator. The reason is that the compiler cannot protect you against bugs introduced by new subclasses. Logic using instanceof is fragile and does not follow the open/closed principle, as it expects only a handful of options. Even if a wildcard case is added, new derivates are still likely to cause errors as they may break assumptions made at the time of writing.

For the cases where you want to distinguish among known alternatives TypeScript has Tagged Unions (also called discriminated unions or algebraic data types). The compiler makes sure that all cases are handled which gives you nice guarantees. The downside is that if you want to add a new entry to the type, you'll have to go through all the code discriminating on it and handle the newly added option. The upside is that such code would have most-likely been broken, but would have failed at runtime.

@gcnew
Copy link
Contributor

gcnew commented Jan 11, 2017

I just gave this proposal a second thought and became against it. The reason is that if throws declarations were present on signatures but were not enforced, they can already be handled by documentation comments. In the case of being enforced, I share the sentiment that they'd become irritating and swallowed fast as JavaScript lacks Java's mechanism for typed catch clauses. Using exceptions (especially as control flow) has never been an established practice as well. All of this leads me to the understanding that checked exceptions bring too little, while better and presently more common ways to represent failure are available (e.g. union return).

@nitzantomer
Copy link
Author

nitzantomer commented Jan 11, 2017

@gcnew
This is how it's done in C#, the problem is that docs aren't as standard in typescript.
I do not remember coming across a definition file which is well documented. The different lib.d.ts files do contain comments, but those do not contain thrown errors (with one exception: lib.es6.d.ts has one throws in Date[Symbol.toPrimitive](hint: string)).

Also, this suggestion takes error inferring into account, something that won't happen if errors are coming from documentation comments. With inferred checked exceptions the developer won't even need to specify the throws clause, the compiler will infer it automatically and will use it for compilation and will add it to the resulting definition file.

I agree that enforcing error handling isn't a good thing, but having this feature will just add more information which can be then used by those who wish to.
The problem with:

... there are better and presently more common ways to represent failure

Is that there's no standard way of doing it.
You might use union return, @Aleksey-Bykov will use Tried<>, and a developer of another 3rd party library will do something completely different.
Throwing errors is a standard across languages (js, java, c#...) and as it's part of the system and not a workaround, it should (in my opinion) have better handling in typescript, and a proof of that is the number of issues I've seen here over time which ask for type annotation in the catch clause.

@HolgerJeromin
Copy link
Contributor

I would love to have information in the tooltip in VS if a function (or called function) can throw. For *.d.ts files we probably need a fake parameter like this since TS2.0.

@nitzantomer
Copy link
Author

@HolgerJeromin
Why would it be needed?

@zpdDG4gta8XKpMCd
Copy link

zpdDG4gta8XKpMCd commented Jan 11, 2017

here is a simple question, what signature should be inferred for dontCare in the code below?

function mightThrow(): void throws string {
   if (Math.random() > 0.5) {
       throw 'hey!';
   }
}

function dontCare() {
   return mightThrow();
}

according to what you said in your proposal it should be

function dontCare(): void throws string {

i say it should be a type error since a checked exception wasn't properly handled

function dontCare() { // <-- Checked exception wasn't handled.
         ^^^^^^^^^^

why is that?

because otherwise there is a very good chance of getting the state of the immediate caller corrupt:

class MyClass {
    private values: number[] = [];

    keepAllValues(values: number[]) {
       for (let index = 0; index < values.length; index ++) {
            this.values.push(values[index]); 
            mightThrow();
       }
    }
}

if you let an exception to slip through you can not infer it as checked, because the behavior contract of keepAllValues would be violated this way (not all values were kept despite the original intent)

the only safe way to is catch them immediately and rethrow them explicitly

    keepAllValues(values: number[]) {
           for (let index = 0; index < values.length; index ++) {
                this.values.push(values[index]); 
                try {
                    mightThrow();
                } catch (e) {
                    // the state of MyClass is going to be corrupt anyway
                    // but unlike the other example this is a deliberate choice
                    throw e;
                }
           }
    }

otherwise despite the callers know what can be trown you can't give them guarantees that it's safe to proceed using code that just threw

so there is no such thing as automatic checked exception contract propagation

and correct me if i am wrong, this is exactly what Java does, which you mentioned as an example earlier

@nitzantomer
Copy link
Author

@Aleksey-Bykov
This:

function mightThrow(): void {
   if (Math.random() > 0.5) {
       throw 'hey!';
   }
}

function dontCare() {
   return mightThrow();
}

Means that both mightThrow and dontCare are inferred to throws string, however:

function dontCare() {
    try {
        return mightThrow();
    } catch (e: string) {
        // do something
    }
}

Won't have a throw clause because the error was handled.
This:

function mightThrow(): void throws string | MyErrorType { ... }

function dontCare() {
    try {
        return mightThrow();
    } catch (e: string | MyErrorType) {
        if (typeof e === "string") {
            // do something
        } else { throw e }
    }
}

Will have throws MyErrorType.

As for your keepAllValues example, I'm not sure what you mean, in your example:

class MyClass {
    private values: number[] = [];

    keepAllValues(values: number[]) {
       for (let index = 0; index < values.length; index ++) {
            this.values.push(values[index]); 
            mightThrow();
       }
    }
}

MyClass.keepAllValues will be inferred as throws string because mightThrow might throw a string and that error was not handled.

@thw0rted
Copy link

thw0rted commented Aug 8, 2022

@jimisaacs I think the argument was made some pages back that we aren't trying to implement an untyped throws clause because that would imply that a function which is not decorated with throws, cannot ever possibly throw, and it's usually unreasonable to try to prove this to the typechecker. If anything, it might be helpful to have a keyword that decorates a function that absolutely cannot ever throw (or a Promise that absolutely cannot ever reject), but that basically means

function f(...): void throws never {
  try {
    ... // Actual logic
  } catch (e) {
    ... // only calling other `throws never` functions
  }
}

or its async equivalent. Instead, as Mr. Rosenwasser said way back at the beginning, the gist is

not to force users to catch the exception, but rather, to better infer the type of a catch clause variable

Re @maugenst , the issue is currently tagged "Awaiting Feedback". 5 years and well over 100 posts does seem like an awfully long time to collect feedback, though. I would be interested in knowing how the team picks issues to discuss in their monthly meetings. You would think nearly a thousand thumbs-ups would grab some attention, right?

@jimisaacs
Copy link

@thw0rted

because that would imply that a function which is not decorated with throws, cannot ever possibly throw, and it's usually unreasonable to try to prove this to the typechecker.

This is an opinion, and I'm sorry but it is one that I either disagree with, or do not fully understand in your use of the words "usually unreasonable" or not.

The typechecker today exists without a throws clause, and as there is no indication that anything can throw, are you saying that currently, the typechecker must assume that anything can throw? I'm sorry, but that is not how people actually use this language and write code today.

Today one can drop a throw within business logic, and with it control flow and discriminate types because you've told the checker about something new. Though there isn't anything outside of that function that you have provided. Now your function can throw, but 🤷. I'm not sure about you, but at least over 50% of the time, that function, that can now throw, won't be wrapped in a try/catch, and is now at risk or will cause a runtime error. This is where a throws clause would have made all the difference.

Outside of what an engineer knows about a function, like the URL constructor throws when you pass an unparsable string, or JSON.parse which throws for the same reason, there is no indication to the typechecker today, and we all can decide what to do about that, while the typechecker just stays out of it. I would strongly argue it's by convention and experience that although the previously mentioned functions have essentially the same purpose, most people wrap JSON.parse in a try/catch, yet forget to do so with new URL. So today, this is all subjective.

In a world where throws does not exists, we make assumptions that only some things can throw based on documentation, trial and error, and actually reading code. Or we let ourselves get lulled into a false sense of security that because something is typed, it's safer from runtime errors, which I think by your logic of this being unreasonable, is the problem with this proposal? This is back to where I either disagree or misunderstand, because without a throws, that has already been my experience with this part of the typescript language.

An untyped throws clause is an indication to both me and the typechecker of what something may or may not do. So yes, as far as something that doesn't have the throws applied yes, it is an indication that something will never throw. That is the actual point of the proposal, to provide a hint of when it would be a good idea to add a try/catch. Not using it on a function is basically no different than today because the proposal doesn't preclude adding try/catch around functions that do not use throws, it is just providing an avenue that would require things that do use it. It's an attempt at removing at least some subjectivity.

Let's look at another example starting with the following:

function getMyThing(obj: Record<string, MyThing>): MyThing {
    return obj.myThing;
}

Now you might be thinking that that code depends on the value of the compiler option noUncheckedIndexedAccess, and you'd be right. Because if noUncheckedIndexedAccess is false, the typechecker will happily assume obj.myThing would just be MyThing, even when in reality it might be undefined. So now the typechecker has lulled us into a false sense of security that we have something that we might not, and we could get bitten by a runtime error. Though because noUncheckedIndexedAccess exists at all, clearly the typescript maintainers / contributors have taken a stronger opinion on this and would like to help with that.

So let's use noUncheckedIndexedAccess and set it to true. With that, now we have to account for undefined, but oh no, I don't want to have to check that everywhere, I want to try and localize it. There is a pattern seen across the community where we can assert with a throw and discriminate the type, so it must be a good idea.

function getMyThing(obj: Record<string, MyThing>): MyThing {
    if (obj.myThing == null) throw key;
    return obj.myThing;
}

Yay! Now we've discriminated the type, but oh no, we've thrown a wrench in there for people to do so. Now everyone must assume getMyThing must throw right? Do they really? My argument is no, they do not.

Though we can help them:

function getMyThing(obj: Record<string, MyThing>): MyThing throws {
    if (obj.myThing == null) throw key;
    return obj.myThing;
}

There. Now I have my discriminated type, but am allowed to properly propagate the cost, that I introduced to get it.

@bensaufley
Copy link

Typed errors would be the major benefit of this feature in my opinion / experience, and going so far as to implement throws without that seems like it would be a weird half-measure. I agree that having a simple throws without typing would at least risk implying that anything that isn't marked throws does not then throw, which is a dangerous road to go down. In fact I would think that it might be reasonable to have a default behavior of throws unknown on functions that do not have explicit error typing, though likely via a strict-type tsconfig option because that would definitely open a can of worms.

I think I disagree with this:

are you saying that currently, the typechecker must assume that anything can throw? I'm sorry, but that is not how people actually use this language and write code today.

I can't speak for how people, broadly, use the language, but I would say I certainly write my code with an assumption that most functions can throw and that at least somewhere up the call stack I'll need to plan for that.

I do like Swift's try helpers and I would love that to be added as syntactic sugar, but I almost think that's the kind of thing you'd want to see in JS (as it doesn't rely on typing) before it found its way to TS.

@jimisaacs
Copy link

jimisaacs commented Aug 8, 2022

@bensaufley I'll try to be brief, as I've written quite a bit here over the course of last day. I just wanted to address a difference, maybe a big one, in the possible interpretations of a typed and untyped throws clause.

The typed throws clause is easily viewed as a better way for error handling. Meaning a way to signal what went wrong, how, the reason, so we may know how to handle it. This is totally a fine interpretation. Error handling is great!

The untyped throws clause is more easily viewed as another form of flow control, i.e. propagation. Meaning a way to signal how errors should be consciously allowed to pass upward, or not. I think an untyped clause actually supports the case of a higher level error handler, because it can be easier to let things pass through, then handle the errors in one place. Though it also gives a better indication of the flow along the way, so that logic can at least be given the chance to handle things lower, even without a typed error, if one chooses.

It seems the typed part of this proposal of a throws clause, is probably mostly to blame for this proposal to be 6 years old at this point? So regardless of whether we agree what is the most valuable part of this proposal, to make progress, maybe it's time to make a new untyped one. Which I guess is what I've attempted to do.

@thw0rted
Copy link

thw0rted commented Aug 8, 2022

@jimisaacs I know this is a super long thread but if you care about this as much as you seem to, it's worth going back over the whole thing. I believe a number of your points are already addressed, better than I could myself.

As @bensaufley put it, I do assume that most functions can throw, which is why I said it's "unreasonable to prove". We can't expect the typechecker to be able to deduce that

function harmless(a: {b: boolean}): void throws never {
  console.log(a.b);
}

is lying, because I passed it an instance of

class A {
  get b(): boolean throws {
    throw new TotallyCustomError("Oops");
  }
}

So, we could have a keyword that says "expect this function to throw in normal operation", then a compiler option that says "flag calls to throw-happy functions", but that would lead to a false sense of security because plenty of un-decorated functions can also throw, and as I showed above, even if you decorate a function to say "I'm pretty sure this can't throw", the compiler cannot verify that. This is why we're saying that typed exceptions add more value than untyped -- it's (nearly) impossible to say that a function won't throw, ergo trivial to say that it might, and thus the primary value in this proposal comes from being able to expose the expected types of exceptions. Everything might throw/reject, all we can ask the typechecker to do to help out is tell us the known exception sources/types.

@jimisaacs
Copy link

I don't understand your last example. Regarding what I have posted recently, I thought it should have been clear that the getter should not pass type checking without a throws. Which in turn would make it clear that harmless is lying. Apologies if I'm misinterpreting it, or if I haven't been clear.

@thw0rted
Copy link

thw0rted commented Aug 8, 2022

I've edited my previous to annotate the getter as "throwing", but the point is that the interface (inline, anonymous, declared as the parameter type in harmless) says nothing about throwing. So, you'd either have to somehow annotate every interface property with its throw-state, exhaustively everywhere -- throws on every property of every method parameter property, throws on the values of mapped types, Record<string, unknown throws>, on and on forever -- or give up and admit that stuff can just, generally, throw unexpectedly, and make the best of it.

@jimisaacs
Copy link

Sounds like the same issue swift had in before 5.5. Didn't support getters before that. Didn't change the fact that it this has value. I'd argue the example is contrived because who would actually throw in a getter anyway?

If anything the proposal would still help in this scenario because if it was explicitly not allowed on getters, then it would be able to actually enforce it statically.

class A {
  get b(): boolean throws { // <- NO, we don't want it on getters
    throw new TotallyCustomError("Oops");
  }
}
class A {
  get b(): boolean 
    throw new TotallyCustomError("Oops"); // <- NO, we can't do this without throws
  }
}
const isThing = (): boolean throws => { throw 'oops'; };
class A {
  get b(): boolean 
    return isThing(); // <- NO, we can't do this without a try/catch
  }
}

@senocular
Copy link

I'd argue the example is contrived because who would actually throw in a getter anyway?

3 examples come to mind

function f() {
    "use strict"
    f.caller // throws
    f.arguments // throws
    arguments.callee // throws
}
f()

@from-nibly
Copy link

from-nibly commented Aug 8, 2022

I'd argue that having throws has value even if none of the existing libs or core libs had any throws annotations on them at all. I know that some will confuse it with "oh this has no throws type associated so there's no way it will throw an error". When the typing is wrong however you could deal with it in one of the following ways.

(non exclusive list)

  • Create an issue with the types
  • Fix the types yourself
  • Complain on your blog about how it's not right
  • Just wrap the method in a try/catch now that you know
  • Annotate the method that calls that function with a throws

I don't think the throws should be held to some sort of guarantee of "this will execute without failure". Java's checked exceptions definitely aren't that.

I'd propose that a throws means "I have coded it and I know an exception can occur and I'm either just letting happen, I'm passing it up, or I'm actually the originator of the exception."

And missing a throws means, "I don't know of any myself", or "I haven't bothered to check", rather that some sort of expectation that it can't throw.

I don't use my types as a strict 100% guarantee that no library creator is going to be lying about types, just that my code isn't lying cause I didn't write any code that's lying.

I don't see why throws would be considered any other way, especially after it first comes out.

Worst case, we could introduce !throws, if you really need to express, "I've done my research and there is no way this method can throw!"

@dead-claudia
Copy link

dead-claudia commented Aug 8, 2022

I have a suggestion here different from all the above: track throws at the statement level, to align with JS completion semantics without burdening the type system with a full concept of a completion.

  • Return values of both functions and function types may be annotated with throws T. If omitted, the default for function types and properties is throws never, and function expressions simply infer from their body.
  • For expression statements, their throws type is the union of the throws types of every expression within them.
    • Function expressions have a throws type of never
    • Variable accesses (outside the obvious with) have a throws type of never
    • All primitive literals (like strings, numbers, and booleans) except template literals have a throws type of never
    • Enum accesses (where valid) have a throws type of never
    • Call expressions have a throws type equal to the union of the throw result of the callee and the throw result of all their parameters.
    • RHS (read) literal property access expressions have a throws type equal to the union of the throw result of their host object and the throws type of the property getter itself. RHS dynamic property access RHS expressions have a throw result equal to the union of the throws types of all possible accesses that could be made and the throw result of the dynamic property access itself.
    • LHS (write) property access expressions work similarly, but check the setter instead of the getter. I additionally propose not adding throws TypeError to account for objects potentially being frozen later, as 1. the readonly modifier is a lot more idiomatic than Object.freeze in TS and 2. how often are people mucking around with descriptors in TS anyways?
    • await and return within async functions of course will need to inspect their inner promise to determine their throws value.
    • Other expressions just have a throws type that's the union of whatever their parameters' throws type are.
  • For throw statements (and possible future expressions), the throws is the union of the expression's throws type and the type of the expression itself.
  • For try/catch, try/finally, and try/catch/finally statements:
    • The throws type of the try block must be assignable to the type of the catch binding, if a catch binding is present.
    • The throws type of the statement itself, if a catch block is present, is the union of the catch block's throws type and the finally block's throws type (if present).
    • The throws type of the statement itself, if no catch block is present, is the union of the try block's throws type and the finally block's throws type.
  • For other statements, return for the throws type the union of all first-level inner statements' and expressions' (for if/switch/etc.) throws types.
  • For the purpose of assignability testing to a variable or parameter, the throws type is ignored. throws assignability is only applied to a function's designated throws type and the value of a catch binding.
  • An extra --strictCatch flag can be added to change the default unknown/any type for catch bindings to the inferred throws type for their corresponding try. This can simplify a lot of exception handling code in practice, especially with Node where basically all file read errors are passed as exceptions.
  • I'm actively not proposing a --inferThrowsNever flag as enforcing checked exceptions in specific areas can be as simple as specifying throws never in a main-like function or in whatever entry points of the library/application. (There's a few places where I would've found an explicit throws ... very useful.)

No code, as I kinda hastily typed all that up (and also likewise there may be some consistency errors in this - please let me know if anything's unclear or inconsistent), but what are your thoughts on this? It allows typed exceptions, opting into checked exceptions piecemeal, and enforcing certain callback contracts like "don't throw" without enforcing checking all of them.

@jimisaacs
Copy link

@senocular sorry, I'm not following that example. Are you saying that is purposeful? Or someone has inadvertently caused a getter to throw by enabling strict mode? If the latter, then "use strict" could be detected and add an assumed throws to those or any other cases, just like you said, and now have yet more information of those possibilities statically.

@jimisaacs
Copy link

jimisaacs commented Aug 9, 2022

@dead-claudia I had missed your message earlier, and was coming back to say I just think this needs a more traditional RFC proposal. Then I saw your message, and wow, that's basically a summary of one right there. 👍

@kevinbarabash
Copy link

It might be worth looking at https://hegel.js.org/ which is a static type checker for JS that tracks what exceptions are thrown by functions and whether they're caught or not.

@dieillusion
Copy link

Any news?

@factoidforrest
Copy link

factoidforrest commented Sep 20, 2022

Edit: this is wrong

What is the rationale behind typescript totally ignoring that Errors exist in JS? Even for flow control, typescript doesn't even realize that if I throw the function will terminate, it still asks me to return something. Granted, just because a function throws, it doesn't mean thats the ONLY thing it could ever throw, but now that typescript is so pervasive in the ecosystem, we could actually have a somewhat complete set of annotations for what functions could throw and what they throw.

It would be extremely helpful to be able to see in my IDE that a function has a good chance of throwing something.

Why does TS just totally ignore errors being part of the language? It's like the one part of the system that still has no typing of any kind, a parallel path of untyped mayhem. I realize that yes, there are a lot of edge cases that make it hard to know exactly what errors are going to be thrown, but FFS, let's at least make an attempt to provide some useful information to the developer.

@Ranguna
Copy link

Ranguna commented Sep 21, 2022

@factoidforrest

typescript doesn't even realize that if I throw the function will terminate, it still asks me to return something.

Hummm... It does not?

This does not throw

const a = (b: boolean) => {
  if (b) return "something";

  throw new Error();
}

But it throws if you comment out the throw.

@factoidforrest
Copy link

I'm trying to recall what flow control issue it was failing to detect, but now I can't make it happen. Looks like I was mistaken about that. My appologies.

@dilame
Copy link

dilame commented Nov 9, 2022

I assume i will not mistake if say this is the most wanted feature in TypeScript

@Ranguna
Copy link

Ranguna commented Nov 9, 2022

@dilame as of time of writing, it's the 5th most commented and the 3rd with the most thumb ups. So probably not the most wanted, but it's close enough.

EDIT: Although it has the most thumb ups out of the ones with most comments, so maybe we could say it's the most active out of the most wanted.

@loynoir
Copy link

loynoir commented Dec 14, 2022

Any news? Or workaround to detect whether need to catch, in case of there is exception in deep scope?

@thw0rted
Copy link

Where I left off was, we still use the eslint rule to flag unhandled then-cases, we include a comment in function documentation if it's guaranteed to never throw, then we annotate each call site with void which makes the rule happy. It's arduous but we did catch a bunch of unhandled errors that way. Sure would be nice if there were language support for automating this process...

@chordmemory
Copy link

I know the typescript team are hesitant to add compiler flags, but surely this is a great use case for them?

Do you want to add strictness all uncaught, known errors that make it up to the top of the callstack? Use an opt in flag.

The folk who don't care can then safely ignore this and still benefit from type inference and suggestions without even realising it.

@loynoir
Copy link

loynoir commented Feb 3, 2023

@thw0rted wow, sounds awesome. Is it available on npm?

@thw0rted
Copy link

thw0rted commented Feb 3, 2023

I believe this is the rule I was talking about. If you're not already using typescript-eslint, that site has docs to get you started.

@loynoir
Copy link

loynoir commented Feb 3, 2023

Ah...I know that. My mistake to understand that as there is workaround to get sync/async function error union type AErr|BErr|CErr

@thw0rted
Copy link

thw0rted commented Feb 3, 2023

Gotcha. So, the linter rule helps you to notice when you're failing to catch a rejection, but as you point out there is still no solution to strongly type the rejected value. (Sure would be nice to have!)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.