X Tutup
The Wayback Machine - https://web.archive.org/web/20201101043927/https://github.com/microsoft/TypeScript/issues/13219
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 · 135 comments
Open

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

nitzantomer opened this issue Dec 29, 2016 · 135 comments

Comments

@nitzantomer
Copy link

@nitzantomer nitzantomer commented Dec 29, 2016

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
Copy link
Member

@DanielRosenwasser DanielRosenwasser commented Dec 30, 2016

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
Copy link
Author

@nitzantomer 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 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);
}
@nitzantomer
Copy link
Author

@nitzantomer nitzantomer commented Jan 10, 2017

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

@zpdDG4gta8XKpMCd zpdDG4gta8XKpMCd commented Jan 10, 2017

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

@aluanhaddad aluanhaddad commented Jan 10, 2017

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

@nitzantomer nitzantomer commented Jan 10, 2017

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

@nitzantomer nitzantomer commented Jan 10, 2017

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

@HolgerJeromin HolgerJeromin commented Jan 11, 2017

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

@nitzantomer nitzantomer commented Jan 11, 2017

@HolgerJeromin
Why would it be needed?

@zpdDG4gta8XKpMCd
Copy link

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

@nitzantomer nitzantomer commented Jan 11, 2017

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

@ivawzh
Copy link

@ivawzh ivawzh commented Jan 29, 2020

I love this suggested feature.
Exception typing and inference can be one of the best things in the entire programming history.
❤️

@Xample
Copy link

@Xample Xample commented Apr 14, 2020

Hello, I just spent a little of time trying to find a workaround with what we do currently have in TS. As there is no way to get the type of the thrown errors within a function scope (nor to get the type of the current method by the way) I figured out on how we could however explicitly set the expected errors within the method itself. We could later, retrieve those type and at least know what could be thrown within the method.

Here is my POC

/***********************************************
 ** The part to hide a type within another type
 **********************************************/
// A symbol to hide the type without colliding with another existing type
const extraType = Symbol("A property only there to store types");

type extraType<T> = {
    [extraType]?: T;
}

// Set an extra type to any other type
type extraTyped<T, E> = T & extraType<E>

// Get back this extra type
type getExtraType<T> = T extends extraType<infer T> ? T : never;

/***********************************************
 ** The part to implement a throwable logic
 **********************************************/

// Throwable is only a type holding the possible errors which can be thrown
type throwable<T, E extends Error> = extraTyped<T,E>

// return the error typed according to the throwableMethod passed into parameter
type basicFunction = (...any: any[]) => any;
const getTypedError = function<T extends basicFunction> (error, throwableMethod:T) {
    return error as getExtraType<ReturnType<T>>;
};

/***********************************************
 ** An example of usage
 **********************************************/

class CustomError extends Error {

}

// Here is my unreliable method which can crash throwing Error or CustomError.
// The returned type is simply our custom type with what we expect as the first argument and the
// possible thrown errors types as the second (in our case a type union of Error and CustomError)
function unreliableNumberGenerator(): throwable<number, Error | CustomError> {

    if (Math.random() > 0.5) {
        return 42;
    }

    if (Math.random() > 0.5) {
        new Error('No luck');
    }

    throw new CustomError('Really no luck')
}

// Usage
try {
    let myNumber = unreliableNumberGenerator();
    myNumber + 23;
}

// We cannot type error (see TS1196)
catch (error) {
    // Therefore we redeclare a typed value here and we must tell the method which could have crashed
    const typedError = getTypedError(error, unreliableNumberGenerator);

    // 2 possible usages:
    // Using if - else clauses
    if (typedError instanceof CustomError) {
        
    }

    if (typedError instanceof Error) {

    }

    // Or using a switch case on the constructor:
    // Note: it would have been really cool if TS did understood the typedError.constructor is narrowed by the types Error | CustomError
    switch (typedError.constructor) {
        case Error: ;
        case CustomError: ;
    }
    
}

// For now it is half a solution as the switch case is not narrowing anything. This would have been 
// possible if the typedError would have been a string union however it would not be reliable to rely
// on typedError.constructor.name (considering I would have find a way to convert the type union to a string union)
@Xample
Copy link

@Xample Xample commented Apr 27, 2020

Thank you very much for your positive feedback! I realise it could be easier to refactor existing code without having to wrap the entire returned type into the throwable type. Instead we could just append that one to the returned type, the following code let you append the throwable errors as follow:

// now only append '& throwable<ErrorsThrown>' to the returned type
function unreliableNumberGenerator(): number & throwable<Error | CustomError> { /* code */ }

This is the only change for the example part, here is the new types declaration:

/***********************************************
 ** The part to hide a type within another type
 **********************************************/
// A symbol to hide the type without colliding with another existing type
const extraType = Symbol("A property only there to store types");

type extraType<T> = {
    [extraType]?: T;
}

// Get back this extra type
type getExtraType<T> = T extends extraType<infer T> ? T : never;

/***********************************************
 ** The part to implement a throwable logic
 **********************************************/

// Throwable is only a type holding the possible errors which can be thrown
type throwable<E extends Error> = extraType<E>

// return the error typed according to the throwableMethod passed into parameter
type basicFunction = (...any: any[]) => any;

type exceptionsOf<T extends basicFunction> = getExtraType<ReturnType<T>>;
 
const getTypedError = function<T extends basicFunction> (error: unknown, throwableMethod:T) {
    return error as exceptionsOf<T>;
};

I also added a new type exceptionsOf which allows to extract the errors of a function in order to escalate the responsibility. For instance:

function anotherUnreliableNumberGenerator(): number & throwable<exceptionsOf<typeof unreliableNumberGenerator>> {
// I don't want to use a try and catch block here
    return (Math.random() > 0.5) ? unreliableNumberGenerator() : 100;
}

As exceptionsOf gets a union of errors, you can escalate as many critical method as you want:

function aSuperUnreliableNumberGenerator(): number & throwable<exceptionsOf<typeof unreliableNumberGenerator> | exceptionsOf<typeof anotherUnreliableNumberGenerator>> {
// I don't want to use a try and catch block here
    return (Math.random() > 0.5) ? unreliableNumberGenerator() : unreliableNumberGenerator();
}

I don't like the use of typeof, if I find a better way I will let you know

You can test here the result tips: hover typedError at line 106

@luisgurmendezMLabs
Copy link

@luisgurmendezMLabs luisgurmendezMLabs commented Apr 28, 2020

@Xample That's a great solution with the tools that we have now!
But I think that in practice it's not enough since you can still do things like:

const a = superRiskyMethod();
const b = a + 1;

And the type of b is infered as number which is correct but just if it's wrapped inside a try
This should not be valid

@ivawzh
Copy link

@ivawzh ivawzh commented Apr 28, 2020

@luisgurmendezMLabs I don't quite get what you mean. If you follow @Xample 's repo on line 56. You can see the result of myNumber + 23 is inferred as number, while myNumber: number & extraType<Error | CustomError>.

In another phase, a in your example is never wrapped in something like a Try Monad. It doesn't have wrapper at all other than an intersection with & extraType<Error | CustomError>.

Congratulations on the awesome design @Xample 👏👏👏👏👏👏. This is really promising, and already useful even without any syntactic sugar. Do you have any plan to build a type library for this?

@luisgurmendezMLabs
Copy link

@luisgurmendezMLabs luisgurmendezMLabs commented Apr 28, 2020

@ivawzh My thoughts are that in practice this might bring some issues since:

function stillARiskyMethod() { 
    const a = superRiskyMethod();
    return a + 1
}

That function type return is infered as number and that's not entierly correct

@Xample
Copy link

@Xample Xample commented Apr 28, 2020

@luisgurmendezMLabs the throwable type is within the return type of the function only as a way to stick the error's type with the function (and to be able to recover those types later). I never return errors for real, I only throw them. Therefore if you are within a try block or not won't change anything.

@ivawzh thank you for asking, I just did it for you

@Xample
Copy link

@Xample Xample commented Apr 29, 2020

@luisgurmendezMLabs ah ok I understand your point, it seems typescript only infer the first type found. For instance if you had:

function stillARiskyMethod() { 
    return superRiskyMethod();
}

the return type of stillARiskyMethod would be inferred correctly, while

function stillARiskyMethod() { 
    return Math.random() < 0.5 superRiskyMethod() : anotherSuperRiskyMethod();
}

would only type the return type as superRiskyMethod() and dismiss the one of anotherSuperRiskyMethod(). I have not investigated more

For this reason, you need to manually escalate the error type.

function stillARiskyMethod(): number & throwable<exceptionOf<typeof superRiskyMethod>> { 
    const a = superRiskyMethod();
    return a + 1
}
@harryjamesuk
Copy link

@harryjamesuk harryjamesuk commented Jun 4, 2020

I also just wanted to drop my thoughts in on this.

I think this would be a really good feature to implement as there are different use cases for wanting to return an Error/null and wanting to throw something and it could have a lot of potential. Throwing exceptions is part of the Javascript language anyway, so why shouldn't we give the option to type and infer these?

For example, if I'm doing lots of tasks that could error, I'd find it inconvenient to have to use an if statement to check the return type each time, when this could be simplified by using a try/catch where if any one of these tasks throws, it'll be handled in the catch without any additional code.

This is particularly useful when you're going to be handling the error's in the same way; for example in express/node.js, I might want to pass the error to the NextFunction (Error handler).

Rather than doing if (result instanceof Error) { next(result); } each time, I could just wrap all of the code for these tasks in a try/catch, and in my catch I know since an exception was thrown I'll always want to pass this to my error handler, so can catch(error) { next(error); }

Also haven't seen this discussed yet (May have missed it however, this thread has quite a few comments!) but if this was implemented, would it be made mandatory (i.e: Compilation error) to have a function that throws without using the throws clause in its function declaration? I feel like this would be nice to do (We're not forcing people to handle the throws, it would just inform them that the function does throw) but the big concern here is that if Typescript was updated in this way, it would likely break lots of currently existing code.

Edit: Another use-case I thought of could be this would also help with generation of JSDocs using the @throws tag

@Akxe
Copy link

@Akxe Akxe commented Jun 6, 2020

I will repeat here what I said in the now committed issue.


I think that typescript should be able to infer the error type of most of the JavaScript expressions. Allowing for faster implementation by library creators.

function a() {
  if (Math.random() > .5) {
    throw 'unlucky';
  }
}

function b() {
  a();
}

function c() {
  if (Math.random() > .5) {
    throw 'unlucky';
  }
  if (Math.random() > .5) {
    throw 'fairly lucky';
  }
}

function d() {
  return eval('You have no IDEA what I am capable of!');
}

function e() {
  try {
    return c;
  } catch(e) {
    throw 'too bad...';
  }
}

function f() {
  c();
}

function g() {
  a();
  c();
}
  • Function a we know that the error type is 'unlucky', and if we want to be very cautious we can extend it to Error | 'unlucky', hence includeError.
  • Function b will inherit the error type of function a.
  • Function c is almost identical; 'unlucky' | 'fairly lucky', or Error | 'unlucky' | 'fairly lucky'.
  • Function d will have to throw unknown, as eval is... unknown
  • Function e catches the error of d, yet since there is throw in the catch block, we infer its type 'too bad...', here, since the block only contains throw 'primitive value' we could say it cannot throw Error (Correct me if I missed some JS black magic...)
  • Function f inherits from c same as b did from a.
  • Function g inherits 'unlucky' from a and unknown from c thus 'unlucky' | unknown => unknown

Here are some compiler options I think should be included, as users engagement with this feature may vary depending on their skill as well as type-safety of libraries they depend on in given project:

{
  "compilerOptions": {
    "errorHandelig": {
      "enable": true,
      "forceCatching": false,   // Maybe better suited for TSLint/ESLint...
      "includeError": true, // If true, every function will by default throw `Error | (types of throw expressions)`
      "catchUnknownOnly": false, // If true, every function will by default throw `unknown`, for those very skeptics (If someone replaces some global with weird proxy, for example...)
      "errorAsesertion": true,  // If false, the user will not be able to change error type manually
    }
  }
}

As for syntax on how to express the error any function can produce, I am not sure, but I know we need the ability for it to be generic and inferable.

declare function getValue<T extends {}, K extends keyof T>(obj: T, key: K): T[K] throws void;
declare function readFile<throws E = 'not valid file'>(file: string, customError: E): string throws E;

My use-case, as showing an actual use-case might show other it has a value:

declare function query<T extends {}, throws E>(sql: string, error: E): T[] throws E;

app.get('path',(req, res) => {
  let user: User;
  try {
    user = query('SELECT * FROM ...', 'get user');
  } catch(e) {
    return res.status(401);
  }

  try {
    const [posts, followers] = Promise.all([
      query('SELECT * FROM ...', "user's posts"),
      query('SELECT * FROM ...', "user's follower"'),
    ]);

    res.send({ posts, followers });
  } catch(e) {
    switch (e) {
      case "user's posts":
        return res.status(500).send('Loading of user posts failed');

      case "user's posts":
        return res.status(500).send('Loading of user stalkers failed, thankfully...');

      default:
        return res.status(500).send('Very strange error!');
    }
  }
});

I need an error sink to prevent sending headers of response multiple times for multiple errors (they usually don't happen, but when they do they do so in bulk!)

@Xample
Copy link

@Xample Xample commented Jul 2, 2020

This issue is still tagged Awaiting More Feedback, what could we do to provide more feedback ? This is the only feature I envy the java language

@christopher-francisco
Copy link

@christopher-francisco christopher-francisco commented Jul 8, 2020

We have a use case where we throw a specific error when an API call returs non-200 code:

interface HttpError extends Error {
  response: Response
}

try {
  loadData()
} catch (error: Error | ResponseError) {
  if (error.response) {
    checkStatusCodeAndDoSomethingElse()
  } else {
    doGenericErrorHandling()
  }
}

Not being able to type the catch block ends up in developers forgetting that 2 possible types of errors can be thrown, and they need to handle both.

@waitingsong
Copy link

@waitingsong waitingsong commented Aug 26, 2020

I prefer to always throw Error object :

function fn(num: number): void {
    if (num === 0) {
        throw new TypeError("Can't deal with 0")
    }
}
try {
 fn(0)
}
catch (err) {
  if (err instanceof TypeError) {
   if (err.message.includes('with 0')) { .....}
  }
}
@lcw0622
Copy link

@lcw0622 lcw0622 commented Aug 27, 2020

Why does this feature still "not enough feedback"? It's so useful when invoking browser's API like indexedDB, localstorage. It's caused many failure in complex scenario but developer can't aware in programing.

@acomagu
Copy link

@acomagu acomagu commented Aug 27, 2020

Hegel seems to have this feature perfectly.
https://hegel.js.org/docs#benefits (scroll to "Typed Error" section)

I wish TypeScript has similar feature!

@Xample
Copy link

@Xample Xample commented Aug 27, 2020

DL;DR;

  • The reject function of the promises should be typed
  • Any type thrown within a try block should be inferred using an union into the error argument of the catch
  • error.constructor should be properly typed using the real type and not only any to prevent missing a thrown error.

Okay, perhaps we should simply clarify the what are our needs and expectations:

Errors are usually handled in 3 ways in js

1. The node way

Using callbacks (which can actually be typed)

Example of usage:

import * as fs from 'fs';

fs.readFile('readme.txt', 'utf8',(error, data) => {
    if (error){
        console.error(error);
    }
    if (data){
        console.log(data)
    }
});

Where fs.d.ts gives us:

function readFile(path: PathLike | number, options: { encoding: string; flag?: string; } | string, callback: (err: NodeJS.ErrnoException | null, data: string) => void): void;

Therefore the error is typed like so

    interface ErrnoException extends Error {
        errno?: number;
        code?: string;
        path?: string;
        syscall?: string;
        stack?: string;
    }

2. The promise way

Where the promise either resolve or reject, while you can type the resolved value, you cannot type the rejection often called the reason.

Here is the signature of a promise's constructor: Note the reason is typed any

    new <T>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void): Promise<T>;

It could theoretically have been possible to type them as follow:

    new <T, U=any>(executor: (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: U) => void) => void): Promise<T>;

In this manner, we could still theoretically make a 1-1 conversion between a node callback and a promise, keeping all the typing along this process. For instance:

const fs = require('fs')
const util = require('util')

const readFilePromise = util.promisify(fs.readFile); // (path: PathLike | number, options: { encoding: string; flag?: string; } | string) => Promise<data: string, NodeJS.ErrnoException>;

3. The "try and catch" way

try {
    throw new Error();
}
catch (error: unknown) { //typing the error is possible since ts 4.0
    if (error instanceof Error) {
        error.message;
    }
}

Despite we are throwing an error "Error" 2 lines before the catch bloc, TS is unable to type the error (value) as Error (type).

Is it not the case neither using promises within an async function (there is no magic "yet"). Using our promisified node callback:

async function Example() {
    try {
        const data: string = await readFilePromise('readme.txt', 'utf-8');
        console.log(data)
    }
    catch (error) { // it can only be NodeJS.ErrnoException, we can type is ourself… but nothing prevents us to make a mistake
        console.error(error.message);
    }
}

There is no missing information for typescript to be able to predict what type of error could be thrown within a try scope.
We should however consider internal, native functions might raise errors which are not within the source code, however if every time a "throw" keyword is in the source, TS should gather the type and suggest it as a possible type for the error. Those types would of course be scoped by the try block.

This is only the first step and there will still be room for improvement, such as a strict mode forcing TS to work like in Java i.e. to force the user to use a risky method (a method which can throw something) within a try block. And if coder does not want to do so, then it would explicitly mark the function as function example throw ErrorType { ... } to escalate the responsibility of handling the errors.

Last but not least: prevent missing an error

Anything can be thrown, not only an Error or even an instance of an object. Meaning the following is valid

try {
    if (Math.random() > 0.5) {
        throw 0
    } else {
        throw new Error()
    }
}
catch (error) { // error can be a number or an object of type Error
    if (typeof error === "number") {
        alert("silly number")
    }

    if (error instanceof Error) {
        alert("error")
    }
}

To know that the error could be of type number | Error would be incredibly helpful. However to prevent forgetting to handle a possible type of error it is not really the best idea to use separate if / else blocs without a strict set of possible outcomes.
A switch case would however do this much better as we can be warned if we forgot to match a specific case (which one would fallback to the default clause). We cannot (unless we do something hackish) switch case an object instance type, and even if we could, we can throw anything (not only an object but also a boolean, a string and a number which have no "instance"). However, we can use the instance's constructor to find out which type it is. We can now rewrite the code above as follow:

try {
    if (Math.random() > 0.5) {
        throw 0
    } else {
        throw new Error()
    }
}
catch (error) { // error can be a Number or an object of type `Error`
    switch (error.constructor){
        case Number: alert("silly number"); break;
        case Error: alert("error"); break;
    }
}

Horray… the only remaining problem is that TS does not type the error.constructor and therefore there is no way to narrow the switch case (yet?), if it would do so, we would have a safe typed error language for js.

Please comment if you need more feedback

@MartinJohns MartinJohns mentioned this issue Oct 10, 2020
5 of 5 tasks complete
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
You can’t perform that action at this time.
X Tutup