-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Can't narrow error type in catch clause #8677
Comments
The workaround is to simply create a local variable: catch (err) {
const ex:Error = err; However it will not help with your desire of narrowing in any meaningful way (I think) 🌹 |
Thanks @basarat, that's what I'll do for now, and it does get narrowing working. It's a pity to need this hack though :( |
Basically a throw can come from anywhere and any object can be thrown :-/ :rose: |
Yep, that part i agree with. What bothers me is the fact that narrowing doesn't work with |
@basarat BTW the hack I'm referring to is the need for both EDIT: -And- the workaround with both |
Isn't try {
noThrow(); // This function's API docs state that it never throws an error
foo(); // This function's API docs state that it either returns or throws a FooError
}
catch (ex: FooError) { // ERROR TS1196
// We could only ever get FooError here
} |
Two interrelated issues being discussed here:
|
Now, having said the above, we could perhaps consider allowing type annotations that specify |
👍 that would at least allow type narrowing and hence better type checking in the catch clause. |
It would be very nice if we could type-annotate the catch clause variable with
The counter-argument that TypeScript should disallow the annotation in all circumstances because it could be incorrect in some circumstances is an interesting one: try {
throw new Error("I'm an error");
}
catch (ex: Error) { // ERROR: We won't allow this annotation even though it's valid
// in this case, because it might be invalid in some other case
...
} ...given that the obvious workaround is to exploit the "user knows best" philosophy that TypeScript applies to an equivalent annotation in a different position: try {
throw new Error("I'm an error");
}
catch (ex) {
let err: Error = ex; // OK, user knows best! And indeed in this case they are correct.
// But if we trust the user then why not just allow annotating `ex`?
...
} |
once the exception caught what is that that you are going to do with it? a rhetorical question, but at the end of the day your options are limited:
function readFileSync(path: string, encoding: string): string | FileDoesNotExist | FileIsLocked {
/* .. */
} |
@Aleksey-Bykov you may be able to recover from some specific error type that you know may be thrown in your |
This could lead to code that is very confusing to read. Catch will catch everything that you might throw at it but the annotation would look like the exception type filter pattern used by many other languages. In general those languages allow multiple catch clauses to be specified in a manner that can be seen as analogous to type pattern matching, or function overloading. The critical difference here is that, in said languages, the behavior is generally such that if no catch clause applies to the runtime shape of the exception it is not caught at all. Well I'm generally opposed to doing things or not doing things simply because they would be intuitive or counterintuitive to programmers coming from other languages, JavaScript explicitly supports only one catch for a reason. Adding a type annotation would be explicitly misleading because it would intuitively suggest that the exception must be of the designated type to be caught. However, if there is indeed a need to support different kinds of exceptions being handled differently, you could try creating a library in the vein of Scala's |
@aluanhaddad the point of this issue is not about allowing a potentially confusing annotation, it's about supporting the practice of checking what was thrown in the catch clause, so that for example recoverable errors can be handled and everything else re-thrown. This is made difficult in TypeScript at present due to the fact that there's currently no way to make type guards work on a variable of type |
Forgive me, if I this has been discussed before, but whats wrong with doing something like this: try {
// ...
} catch (e: FooException) {
// ...
} catch (e: BarException) {
// ...
} catch (e) {
// ...
} Which would generate JS like this: try {
// ...
} catch (e) {
if (e instanceof FooException) {
// do stuff in foo block
} else if (e instanceof BarException) {
// do bar block
} else {
// catch all block
}
} If there is no catch all block, then just re-throw the exception: try {
// ...
} catch (e: FooException) {
// ...
} JS: try {
// ...
} catch (e) {
if (e instanceof FooException) {
// do stuff in foo block
} else {
throw e;
}
} And in the type annotations would maybe only accept constructors? So what's "instanceof-able" in JS? Maybe also |
@bali182 What you are suggesting would use results from the type system (the |
I was about to file something about this but I see there's already an issue... I would like to frame the problem this way. This is a really standard Javascript idiom (I've seen it recommended in pure-JS stackoverflow answers more than once; here's one example):
Not only is it a common idiom, because of the limitations of Javascript as a language there is no better way to do it if you are doing exceptions with ES6 classes. In all other cases I've seen, Typescript takes common Javascript idioms and finds some way to make them typesafe, even if the result is a little awkward. However Typescript fails in this case to give me a convenient way to make a common, mandatory even, part of Javascript programming typeable. I think this means Typescript is not living up to its promise in this case, and it should be fixed somehow. That could mean type-narrowing on if ( x isinstance e ), it could mean a special ifinstance(x, e) or an explicitly narrowing variant of isinstance, it could mean bali182's proposal. I do like bali182's proposal best because it would produce more convenient code that looks like other languages. It would shorten the simple catch clause in my example from six lines (eight, if I choose to introduce a second typed variable for e) to three. I don't agree with the idea this interferes with Typescript's goal of being an erasing compiler. There are other situations where Typescript introduces a syntax construct completely absent from javascript-- enum {} would be an example-- if it is necessary to fill out the type system. Multiple catch{}es are not legal Javascript and typed catches are not legal Typescript so this is not altering behavior based on type so much as introducing a new syntax construct that compiles to idiomatic Javascript. If there's something philosophically unpleasant about the fact that what triggers the multi-catch is specifically a type annotation, maybe the syntax could be Incidentally, there is a conditional catch clause syntax Firefox has introduced as a js extension and which may be relevant to be aware of here. However it is not on a standards track, and it is also very ugly. |
@mcclure Because code is being generated based solely on types. import { mayThrow, AccessError } from './lib';
try {
mayThrow();
} catch (e instanceof AccessError) {
console.error(e.accessViolationMessage)
} catch (e) {
throw e;
} lib.d.ts export declare class AccessError {
accessViolationMessage: string;
}
export function mayThrow(); lib.js exports.mayThrow = function () {
if (true) {
throw new exports.AccessError();
}
};
exports.AccessError = function () {
var e = new Error();
e.accessViolationMessage = 'Not logged in.';
return e;
}; The error will not be caught, nor the message printed. This code is extremely misleading and the bug will be very hard to track down. See #9999 for a more reasonable alternative. |
@aluanhaddad So, in your example, it seems like the Typescript type checker only does something incorrect because the ambient typings are incorrect. It seems like by nature Typescript will typecheck incorrectly if its ambient type declarations are incorrect. This said, Typescript does not really do anything incorrect in your example. Moreover, we would have all of these same problems both with the buggy behavior and the type checking if we wrote the idiomatic Javascript in a scenario where the narrowing instanceof (bug 9999 you reference) were implemented. If bug 9999 were implemented and one used the |
@mcclure you're absolutely correct that this suffers from the same problems. The problem I have with @bali182's suggested solution is that it bakes an unreliable pattern into the language, encouraging its use by providing manifest syntax, and providing a false sense of security to the programmer. With #9999 a restriction is being lifted, but you still have to choose to shoot yourself in the foot and nothing encourages you to do so. |
I'm sorry, maybe I am confused :( What is unreliable about the pattern? It seems like it is completely reliable if you have an accurate d.ts file. |
@mcclure Personally I don't think your suggestions are 'unreliable' any more or less that other patterns in TypeScript which can also be made unreliable if we try hard enough. The thing that makes @bali182's original suggestion likely incompatible with TypeScript's design goals is that the following three examples only differ by a type annotation, but all would generate different JS code with different runtime behaviour: try { foo(); } catch (ex) {/***/}
try { foo(); } catch (ex: Error) {/***/}
try { foo(); } catch (ex: FooError) {/***/} Type annotations are there to inform the compiler about what the program actually does, but they never change what it does. With these 'hints', the type checker can detect invalid usage at compile-time and produce useful early feedback to the developer. If you strip out all the type annotations in a program, that would not change the meaning/behaviour of the program. But it would in the above example. Your other suggestion (multiple But mainly, the problems in this issue would simply vanish if TypeScript just allowed the try {
foo();
}
catch (ex) {
if (ex instanceof FooError) {
/* recover from FooError */
ex.foo
...
}
else {
throw ex;
}
} Thas already works at runtime and compiles without errors too, but is just not typesafe because |
@mcclure I am saying that var x = { name: 'Bob', age: 35 };
Reflect.setPrototypeOf(x, Date.prototype);
console.log(x instanceof Date) // true
x.getHours(); // Error @yortus you are correct that there plenty of other patterns that can be unreliable, but TypeScript is fairly agnostic with respect to whether you use them. As you point out this is a big thing to ask for, in other words, it is a new language feature. |
We'll handle this at #9999 |
I guess this is expected behaviour not a bug. But I was not previously aware that the catch clasue variable cannot be annotated. Due to #1426, this means we can't narrow
ex
inside the catch clause, so all the cases dealing with specific error types get no type checking (sinceex
remains asany
).The text was updated successfully, but these errors were encountered: