microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
100.89k stars 12.47k forks source link

Suggestion: `throws` clause and typed catch clause #13219

Closed nitzantomer closed 1 year ago

nitzantomer commented 7 years ago

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 commented 7 years ago

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?

nitzantomer commented 7 years ago

@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 commented 7 years ago

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);
}
nitzantomer commented 7 years ago

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

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 commented 7 years ago

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

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

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:

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@HolgerJeromin Why would it be needed?

zpdDG4gta8XKpMCd commented 7 years ago

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 commented 7 years ago

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

zpdDG4gta8XKpMCd commented 7 years ago

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

I meant the exceptions coming unhandled from mightThrow interrupt keepAllValues and make it finish in a middle of what it was doing leaving its state corrupt. It is a problem. What you suggest is to close your eyes on this problem and pretend it's not serious. What I suggest is to address this problem by requiring that all checked exceptions are immediately handled and explicitly rethrown. This way there is no way to get the state corrupt unintentionally. And although it can still be corrupt if you choose so, it would require some deliberate coding.

Think about it, there are 2 ways we can go about exceptions:

now if we decided to go with the checked exceptions which are properly handled and prevent a crash we need rule out a situation when we handle an exception coming from several layers deep of where you are catching it:

export function calculateFormula(input) {
    return calculateSubFormula(input);
}
export function calculateSubFormula(input) {
   return calculateSubSubFormula(input);
}
export function calculateSubSubFormula(input): number throws DivisionByZero  {
   return 1/input;
}

try {
   calculateFormula(0);
} catch (e: DivisionByZero) {
   // it doesn't make sense to expose DivisionByZero from under several layers of calculations
   // to the top level where nothing we can do or even know what to do about it
   // basically we cannot recover from it, because it happened outside of our immediate reach that we can control
}
zpdDG4gta8XKpMCd commented 7 years ago

the example above brings a interesting case for consideration, what would be the inferred signature of:

function boom(value: number) /* what comes here?*/  {
    return 1/value;
}
zpdDG4gta8XKpMCd commented 7 years ago

another interesting case


// 1.
function run<R, E>(callback(): R throws E) /* what comes here? */ {
    try {
        return callback();
    } catch (e: DivisionByZero) {
        // ignore
    }
}

function throw() { return 1 / 0; }

// 2.
run(throw); /* what do we expect here? */
nitzantomer commented 7 years ago

@aleksey-bykov So you propose that all errors must be handled like it is with java?
I'm not a fan of that (even though I come from java and still loving it) because js/ts are way more dynamic and their users are accustomed to that.
It can be a flag that makes you deal with errors if you include it when compiling (like strictNullChecks).

My suggestion isn't here to solve unhandled exceptions, the code you posted will break now without this feature implemented, and it would break in js as well.
My suggestion just let you as a developer be more aware of the different errors that might be thrown, it's still up to you if to handle them or ignore them.

As for the division by 0 issue, it doesn't result in an error:

console.log(1 / 0) // Infinity
console.log(1 / "hey!") // NaN
zpdDG4gta8XKpMCd commented 7 years ago

more aware of the different errors that might be thrown

there is no point of doing so unless they can deal with them, the current proposal isn't viable because of the cases i listed

So you propose that all errors must be handled like it is with java?

yes, this is what it means having checked exceptions

nitzantomer commented 7 years ago

@aleksey-bykov I don't see why any of the cases you listed render this proposal as inviable.

There's no problem with handling an error that was thrown way down the invocation chain, even if I'm using a function that was inferred of throwing DivisionByZero (regardless of where it was thrown), I can choose to handle it.
I can try to re-try it with different arguments, I can show the user a message that something went wrong, I can log this problem so that I can later change my code to handle it (if it happens often).

Again, this proposal doesn't change anything in runtime, so everything that worked will continue to work as before.
The only difference is that I will have more information about the errors that might be thrown.

zpdDG4gta8XKpMCd commented 7 years ago

i see what you are saying, nothing is going to be changed at javascript runtime, however your message here is to give users some illusion that they know what they are doing by handling an exception that came from 20 layers down below with the same confidence as they would handle an immediate exception

there is simply no way they can fix a problem that happened 20 layers down below

you can log it, sure, just as any unchecked exception, but you cannot fix it

so it's a lie generally speaking, there is enough lies in TS, let's not confuse people even more

nitzantomer commented 7 years ago

@aleksey-bykov What you're describing exists in all languages that support exceptions.
No one said that catching an exception will fix the problem, but it will let you handle it gracefully.

Knowing which errors can be thrown when invoking a function will help the developers to separate between the errors that they can handle and those which they can't.

Right now developers might not know that using JSON.parse might throw an error, but if it was part of the lib.d.ts and the IDE would let him know (for example) then maybe he'll choose to handle this case.

zpdDG4gta8XKpMCd commented 7 years ago

you can't handle a problem happened 20 layers below gracefully, because the internal state is corrupt in 19 layers and you can't go there because the state is private

to be constructive: what i am suggesting is to require users handle checked exceptions immediately and rethrow them explicitly, this way we rule out unintended confusion and separate checked exceptions from unchecked:

zpdDG4gta8XKpMCd commented 7 years ago

SyntaxError in JSON.parse should be declared as a checked exception

nitzantomer commented 7 years ago

@aleksey-bykov

I don't see why there's a need to enforce developers to do something they don't wish to, something that they haven't done so far.

Here's an example: I have a web client in which the user can write/paste json data, then click a button. The app takes this input and passes it to a 3rd party library that somehow parses this json and returns the json along with the different types of the values (string, number, boolean, array, etc). If this 3rd party library throws a SyntaxError I can recover: inform the user that his input is invalid and he should try again.

By knowing which errors can be thrown when invoking a function the developer can decide what he can/wishes to handle and what not.
It shouldn't matter how deep in the chain the error was thrown.

zpdDG4gta8XKpMCd commented 7 years ago

look you don't seem to get what i am saying, we are going in circles

by letting SyntaxError thrown from 3rd party library you are exposing your user to the implementation details of your own code which are supposed to be encapsulated

basically you are saying, hey, it's not my code that doesn't work, it's that stupid library that i found on the internet and used, so if you have a problem with it, deal with that 3rd party lib, not me, i just said what i was asked to

and there is no guarantee that you can still use the instance of that 3rd lib after that SyntaxError, it's your responsibility to provide guarantees to the user, say by reinstantiating the 3rd party control after it threw

bottom line, you need to be in charge for handling inner exceptions (not all of them, only the checked ones, i beg you)

nitzantomer commented 7 years ago

I am getting what you're saying, but I don't agree with it. You're right, that is basically what I'm saying. If I used a 3rd party library that throws an error I can choose to deal with it or ignore it and let the user of my code handle it. There are many reasons to do so, for example the lib that I'm writing is UI-agnostic, so I can't inform the user that something is wrong, but who ever uses my lib can handle the errors that are thrown when using my lib and handle them by interacting with the user.

If a library is left with a corrupted state when it throws, then it probably needs to document it.
If I then use such a library and as a result in an error in it my state becomes corrupted then I need to document it.

Bottom line: This suggestion comes to offer more information about thrown errors. It shouldn't enforce developers to do things differently, just make it easier on them to deal with the errors if they choose to.

zpdDG4gta8XKpMCd commented 7 years ago

you can disagree, it's fine, let's just not call them checked exceptions please, because the way you put it isn't what checked exceptions are

let's call them listed or revealed exceptions, because all you care is to make developers aware of them

nitzantomer commented 7 years ago

@aleksey-bykov Fair enough, name changed.

edevine commented 7 years ago

@aleksey-bykov

you can't handle a problem happened 20 layers below gracefully, because the internal state is corrupt in 19 layers and you can't go there because the state is private

No you cannot fix the internal state, but could certainly fix the local state, and that's exactly the point of handling it here and not deeper in the stack.

If your argument is that there is no way to be certain what state some shared mutable values are in when handling the exception, then it's an argument against imperative programming, and not confined to this proposal.

zpdDG4gta8XKpMCd commented 7 years ago

if every layer is bound to take responsibility of reacting to an exception coming immediately from a layer below, there is a much better chance for a successful recovery, this is the idea behind the checked exceptions as i see it

to put it in different words, exceptions coming from more than 1 level below is a sentence, it's too late to do anything other than re-instantiating all infrastructure from ground up (if you are lucky enough there are no global leftovers that you can't reach)

proposal as stated is mostly useless, because there is no reliable way to react to the knowledge of something bad happened outside of your reach

agonzalezjr commented 7 years ago

This is great. FWIW: I think if added, it should be required by default to handle throwing methods or mark your method as throwing as well. Otherwise it's just documentation pretty much.

nitzantomer commented 7 years ago

@agonzalezjr I think that like most features in typescript you should be able to opt-in with this feature as well. Just like it's not mandatory to add types, it shouldn't be a must to throw/catch.

There probably should be be a flag to make it a must, like --onlyCheckedExceptions.

In any case, this feature will also be used to infer/validate the types of thrown exceptions, so not just for documentation.

aluanhaddad commented 7 years ago

@nitzantomer

Here's an example: I have a web client in which the user can write/paste json data, then click a button. The app takes this input and passes it to a 3rd party library that somehow parses this json and returns the json along with the different types of the values (string, number, boolean, array, etc). If this 3rd party library throws a SyntaxError I can recover: inform the user that his input is invalid and he should try again.

This is certainly one area where the whole idea of checked exceptions becomes murky. It is also where the definition of exceptional situation becomes unclear. The program in your example would be an argument for JSON.parse being declared as throwing a checked exception. But what if the program is an HTTP client and is calling JSON.parse based on the value of a header attached to an HTTP response that happens to contain an ill-formed body? There is nothing meaningful the program can do to recover, all it can do is rethrow. I would say that this is an argument against JSON.parse being declared as checked.

It all depends on the use case.

I understand that you are proposing that this be under a flag but let's imagine that I want to use this feature so I have enabled the flag. Depending on what kind of program I am writing, it may either help or hinder me.

Even the classic java.io.FileNotFoundException is an example of this. It is checked but can the program recover? It really depends on what the missing file means to the caller, not the callee.

nitzantomer commented 7 years ago

@aluanhaddad

This suggestion doesn't propose to add any new functionality, only to add a way to express in typescript something that already exists in javascript. Errors are thrown, but currently typescript has no way of declaring them (when throwing or catching).

As for your example, by catching the error the program can fail "gracefully" (for example showing the user a "something went wrong" message) by catching this error, or it can ignore it, depending on the program/developer. If the programs' state can be affected by this error, then handling it can keep a valid state instead of a broken one.

In any case, the developer should make the call of whether he can recover from a thrown error or not.
It's also up to him to decide what it means to recover, for example if I'm writing this http client to be used as a 3rd party library, I might want all errors thrown from my library to be of the same type:

enum ErrorCode {
    IllFormedJsonResponse,
    ...
}
...
{
    code: ErrorCode;
    message: string;
}

Now, in my library when I parse the response using JSON.parse I want to catch a thrown error and then throw my own error:

{
    code: ErrorCode.IllFormedJsonResponse,
    message: "Failed parsing response"
} 

If this feature is implemented then it will be easy for me to declare this behavior and it will be clear to the users of my library how it works and fails.

aluanhaddad commented 7 years ago

This suggestion doesn't propose to add any new functionality, only to add a way to express in typescript something that already exists in javascript.

I know. I'm talking about the errors that TypeScript would emit under this proposal. My assumption was that this proposal implied a distinction between checked and unchecked exception specifiers (inferred or explicit), again only for type checking purposes.

nitzantomer commented 7 years ago

@aluanhaddad

What you said in the previous comment:

But what if the program is an HTTP client and is calling JSON.parse based on the value of a header attached to an HTTP response that happens to contain an ill-formed body? There is nothing meaningful the program can do to recover, all it can do is rethrow.

Applies the same for returning a null when my function is declared to return a result.
If the developer chooses to use strictNullChecks then you can say the exact same thing if the function returns a null (instead of throwing) then in the same scenario "there is nothing meaningful the program can do to recover".

But even without using a onlyCheckedExceptions flag this feature is still useful because the compiler will complain for example if I try to catch the error as a string when the function is declared to throw only Error.

shaipetel commented 7 years ago

Nice Idea, it would be helpful but not strict/type safe since there is no way of knowing what nested calls might throw at you.

Meaning, if I have a function that might throw an exception of type A, but inside I call a nested function and don't put it in try catch - its going to throw its type B exception to my caller. So, if the caller expects only type A exceptions there is no guarantee he won't get other types from nested exceptions.

(the thread is too long so - sorry if i missed this comment)

nitzantomer commented 7 years ago

@shaipetel The proposition states that the compiler will infer the types of unhandled errors and will add them to the function/method signature. So in the case you described your function will throw A | B in case B was not handled.

shaipetel commented 7 years ago

Oh, I see. It will drill down through all methods I call and collect all possible exception types? I would love to see it happen, if it is possible. See, a developer can always have an unexpected exception that won't be declared, in which case an "object not set to instance" or "divide by 0" or similar exceptions are always possible almost from any function. IMHO, it would have been best handled like in C# where all exceptions inherit from a base class that has a message and not to allow at all the throw unwrapped text or other objects. If you have base class and inheritance you can cascade your catches and handle your expected error in one block, and other unexpected in another.