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
Comments
|
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 But it will give developers a way to express which exceptions can be thrown (would be awesome to have that when using other libraries |
|
how is a checked throw different from 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);
} |
|
You're suggesting not to use
Adding
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 |
|
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
);
} |
|
My point with You can represent the same bad situation with throwing an error. 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. 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. Will work as expected in javascript, just without the type annotation. Using |
|
if we talking about browsers 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) {} |
|
besides class StandardError {}
class CustomError extends StandardError {
}
function doSomething() { throw new CustomError(); }
function oldCode() {
try {
doSomething();
} catch (e) {
if (e instanceof StandardError) {
// problem
}
}
} |
|
@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. |
|
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. |
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 |
|
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. |
|
Back to the OP question - |
|
Developers should be aware of the different js issues you described, after all adding The fact that 3rd party libraries ca throw errors is exactly my point. @aluanhaddad @gcnew |
|
@nitzantomer Subclassing native classes ( |
|
@gcnew In anycase this suggestion doesn't assume that the user is subclassing the Error class, it was just an example. |
|
@nitzantomer I'm not arguing that the suggestion is limited to
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. |
|
I just gave this proposal a second thought and became against it. The reason is that if |
|
@gcnew 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 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.
Is that there's no standard way of doing it. |
|
I would love to have information in the tooltip in VS if a function (or called function) can throw. For |
|
@HolgerJeromin |
|
here is a simple question, what signature should be inferred for 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 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 |
|
@Aleksey-Bykov Means that both Won't have a Will have As for your
|
|
@jimisaacs I think the argument was made some pages back that we aren't trying to implement an untyped function f(...): void throws never {
try {
... // Actual logic
} catch (e) {
... // only calling other `throws never` functions
}
}or its
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? |
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 Today one can drop a Outside of what an engineer knows about a function, like the In a world where An untyped 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 So let's use 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. |
|
Typed errors would be the major benefit of this feature in my opinion / experience, and going so far as to implement I think I disagree with this:
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 |
|
@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. |
|
@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. |
|
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. |
|
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 |
|
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
}
} |
3 examples come to mind function f() {
"use strict"
f.caller // throws
f.arguments // throws
arguments.callee // throws
}
f() |
|
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)
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 And missing a 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!" |
|
I have a suggestion here different from all the above: track
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. |
|
@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. |
|
@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. |
|
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. |
|
Any news? |
|
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 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. |
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 |
|
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. |
|
I assume i will not mistake if say this is the most wanted feature in TypeScript |
|
@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. |
|
Any news? Or workaround to detect whether need to catch, in case of there is exception in deep scope? |
|
Where I left off was, we still use the eslint rule to flag unhandled |
|
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. |
|
@thw0rted wow, sounds awesome. Is it available on npm? |
|
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. |
|
Ah...I know that. My mistake to understand that as there is workaround to get sync/async function error union type |
|
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!) |
The typescript type system is helpful in most cases, but it can’t be utilized when handling exceptions.
For example:
The problem here is two fold (without looking through the code):
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 betype | Error.The grammar is straightforward, a function definition can end with a throws clause followed by a type:
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:
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:
Compiles with no errors, but:
Fails to compile because
numberisn'tstring.Control flow and error type inference
Throws
string.Throws
MyError | string.However:
Throws only
MyError.The text was updated successfully, but these errors were encountered: