TypeScript Version: 2.6.1
const rejected = Promise.reject(new Error());
async function tryCatch() {
try {
await rejected;
} catch (err: Error) { // TS1196: Catch clause variable cannot have a type annotation
// Typo, but `err` is `any`, so it results in runtime error
console.log(err.mesage.length);
}
}
function promiseCatch() {
rejected.catch((err: Error) => { // OK
// Compiler error; Yay!
console.log(err.mesage.length);
});
}
This was discussed in #8677 and #10000. It was closed as "fixed" in #9999, but as far as I can tell neither of the issues was actually resolved. In either event, I'd like to make a case for allowing type annotations in catch clauses.
Especially with the introduction of downlevel async functions, I'd suggest that disallowing catch clause type annotations leads to less safe code. In the example, the two methods of handling the promise are functionally equivalent, but one allows you to type the error, and the other doesn't. Without writing _extra_ code for the try
/catch
version (if (err instanceof Error) {
or const e: Error = err
or something), you'll get a runtime error that you wouldn't get with the pure Promise
version.
The primary rationale for not allowing this is that any object can be thrown, so it's not guaranteed to be correct. However, most of the benefit of TypeScript comes from making assertions about your and other people's code that can't be strictly guaranteed (especially when importing JavaScript). And unless one would argue that the Promise
catch function _also_ shouldn't allow a type annotation on the error parameter, this argument seems to make very little practical sense.
I believe one of the other arguments against is that it might be confusing, as it looks like the typed exception handling you might see in other languages (e.g., Java), and folks may think the catch will only catch errors of the annotated type. I don't personally believe that's a legitimate issue, but if it really is I'd propose at least allowing a catch (err as Error) {
syntax or similar as a way of emphasizing that it's a type _assertion_.
If nothing else at all, it seems that there should be a way to trigger a warning (similar to an implicit any warning) when using an untyped err
directly within a catch block.
I do like the idea of catch (err as Error)
because, damn it, if you were going to cast it anyway, maybe we should make your life a little easier while keeping it explicit.
I agree, it is possible to safely annotate the error variable in many situations. Older discussion in https://github.com/Microsoft/TypeScript/issues/8677#issuecomment-220495646.
It also true that it's easy to misunderstand/abuse, but then so are type annotations in general, and people just do the cast in the first line of the catch block anyway.
@jaredru code like
function promiseCatch() { rejected.catch((err: Error) => { // OK // Compiler error; Yay! console.log(err.mesage.length); }); }
is dangerous and misleading. It only passes the type checker because the parameter of the catch method's callback is declared to be
any
. At best this is a disguised type assertion.
Promise.reject(NaN).catch((e: Error) => e.message.length);
I disagree. It is no more dangerous or misleading than const foo: Foo = JSON.parse(input);
, which is a very reasonable pattern. It represents our expectations and assertions about the code we're dealing with.
The irony of your example is that it throws a TypeError
, which gets at my underlying point. Aside from contrived examples, the expected currency for errors is Error
. A simple search through the TypeScript repo itself shows that catch (e)
virtually always assumes the e
to be an Error
. That doesn't make it "right", of course, but it highlights the realistic expectation of a large, real-world project that (presumably) reflects an understanding of the balance between correctness and pragmatism, particularly as it relates to TypeScript.
I disagree. It is no more dangerous or misleading than
const foo: Foo = JSON.parse(input);
, which is a very reasonable pattern. It represents our expectations and assertions about the code we're dealing with.
I don't think that is a good pattern either.
const foo = <Foo>JSON.parse(input);
Is much clearer as to intent.
As for the example, contrived as it may be, it doesn't matter what gets thrown (TypeError
or not) because it is thrown from the catch
causing an unexpected failure.
Sorry, consider it a typo. <Foo>JSON.parse(s)
is a common pattern but is as unsafe as .catch((e: Error) => {
. Both treat an any
as another type with no guarantee of correctness beyond the developer's word.
it doesn't matter what gets thrown (TypeError or not) because it is thrown from the catch causing an unexpected failure.
Of course it matters. That the runtime throws an Error
for all runtime errors strengthens the position that it's reasonable to expect the argument to catch
to be an Error
. This expectation is common in real world codebases--including, but certainly not limited to, TypeScript's.
My 2ct: If there is any party that should be able to type the exception it should be the called function, not the caller. I've seen codebases that throw strings everywhere or nothing at all (undefined
). Just declaring that the type is Error
doesn't make it more type safe, because you never know what other functions the function you are calling may call into and what things those nested functions throw - Errors are not strongly typed and not part of the interface contract. Maybe you know it now but in the next patch version of some transient dependency it could change. So the only correct way to mirror the runtime JS is to start with any
unknown
and narrow it down manually
if (err instanceof MyError) ... else throw err
switch (err && err.code) {
case 'ENOENT': ...
case 'EPERM': ...
default: throw err
}
const isExpectedHTTPError = (err: any): err is HTTPError => err && typeof err.status === 'number'
if (isExpectedHTTPError(err)) {
res.status(err.status).set(err.headers || {}).send(err.body || '')
} else {
res.status(500)
}
And if the check doesn't pass, rethrow the error. If you don't and your usage of the error doesn't directly produce some kind of TypeError then you might swallow unexpected programming errors and create a debugging nightmare.
Is this more code to write? Yes. But does that justify a new feature to make the unsafe version easier to write?
My argument is for pragmatism. One of the stated _non_-goals of TypeScript's design is to "Apply a sound or 'provably correct' type system. Instead, strike a balance between correctness and productivity." Yes, I _could_ get something besides an Error
, but in reality--in the projects I'm working in--I won't.
As in the TypeScript codebase itself (1 2 3 4 5), it is very often safe and reasonable to assume that the thrown value is not, in fact, undefined
(or null
, or a string, ...) and is with almost certainty an Error
. I would simply like the compiler to help me out in this case without additional code or runtime overhead.
TypeScript language is already the best I've ever worked.
If you type the catch clause this will make much better.
I'd like to at least be able to do
try {
await rejected;
} catch (err: unknown) {
// ...
}
I would certainly love a exceptionTypeUnknown
flag that makes all exception types (and Promise rejections) type unknown
instead of any
. That would definitely make the error handling _safer_ as opposed to allowing any annotation. Although this could also be solved by writing a TSLint rule that forces them to always be declared unknown
, if we had this annotation.
need this
It's not productive for me to guess which properties are on the Error and have to look it up. Please implement.
@DanielRosenwasser
I do like the idea of
catch (err as Error)
because, damn it, if you were going to cast it anyway, maybe we should make your life a little easier while keeping it explicit.
I think it would be awesome !
I like @Andrew5569 's Idea, but want to have the ability to define it like I want:
try {
// ...
} catch (err: unknown) {
// ...
}
try {
// ...
} catch (err: Error) {
// ...
}
try {
// ...
} catch (err: AWS.AWSError) {
// ...
}
@Shinigami92 It will never be possible. TS is being transcoded to Javascript for execution. And Javascript does not implement the type.
What would be possible is
try {
// ...
} catch (err: unknown | Error | AWS.AWSError) {
if (err instanceof Error) {
// ...
} else if (err instanceof AWS.AWSError) {
// ...
} else {
// ...
}
}
@GabrielDelepine sorry you get me wrong. I only want to assume the type, not to define it.
Yeah, please just fix this :-)
The fact that this is not allowed is both inconsistent and inconvenient:
try { ... } catch(error: Error) { ... }
Especially considering that this _is_ allowed:
promise.catch((error: Error) => ... )
You can't argue that allowing it would be dangerous, just because people might incorrectly assume the type is Error
, when it can in fact be anything. If someone were to assumes that, they will just cast to Error
anyway, which would be just as much of an error. And this could be said for any type used anywhere, so there's really no valid reason to treat errors as a special case.
Not to mention, that in the vast majority of cases, assuming that the error will in fact be an Error
, _is_ actually perfectly reasonable. Please be a bit more pragmatic about those things, especially when it is something that gets in our way on a daily basis.
While you are at it, please also add an optional reject type to promises:
interface Promise<TResolve, TReject = any>
Because yes, we _can_, and often _do_, guarantee that an async function will always reject with a certain type - and arguing against that is kinda ridiculous. We can just wrap everything in the function in a try
block, and throw a custom error if we end up in the catch
.
For example, we have an ApiClient
, which guarantees that if it rejects, it will always be with an ApiError
. It's _extremely_ annoying that we can't declare that fact in our types, but instead have to manually cast the errors everywhere we use it. That's neither practical, nor type safe.
These would not be breaking changes, they would provide _a lot_ of value, and people have been asking for it for years - so please just implement it :-)
I would love it if it were implemented with a new Throws<TReason = Error>
type and Promise<TResolve, TReason>
for Promises.
function foo(): Throws<Error> { // should be inferred
throw new Error()
}
try {
foo()
} catch (error) { // <-- Error
// noop
}
Error
(e.g., a number
)function bar(): Throws<number> { // should be inferred
throw 42;
}
try {
bar()
} catch (code) { // <-- number
// noop
}
try {
foo();
bar();
} catch (reason) { // <-- Error | number
// noop
}
async function baz(): Promise<never, TReason = Error> { // should be inferred
return Promise.reject(new Error())
}
async function qux() {
try {
await baz()
} catch (err) { // <-- Error
// noop
}
}
My suggestion meets these guidelines:
Throws<TReason = any>
, but that's not really a sensible default, as discussed above.@jedmao
Why do you think your approach does not accept this?
function foo(): Throws<Error> { // should be inferred
throw new Error()
}
try {
const err: Throws<Error> = foo()
} catch (error) { // <-- Error
// noop
}
I also think that Throws<TReason = Error>
introduces a breaking change when it is inferred (and it should).
Throws<TReason = any>
would not do this.
@Shinigami92 good point on the breaking change.
I don't think your example is any different than the never
type.
function foo(): never {
throw new Error();
}
try {
const err: never = foo()
} catch (error) { // <-- any (existing implementation)
// noop
}
+1 for having type-check flow through try/throw/catch
code. This is driving me nuts currently for adopting async/await with try/catch where now I have no way of typing the error (with promises I could at least add a type).
At the very least, you should be allowed to use a type that extends the Error type, as is done with @types/mysql
.
try {
// perform database fetch
} catch (error: MysqlError) {
if (error.code === 'ER_DUP_ENTRY') {
return;
} else {
throw error;
}
}
The catch (err as Error)
syntax seems related to (or is possibly a subset of) #10421
Is there any hope for this feature? (or something better)
I think a better solution would be to allow typed catch
blocks to be transpiled to if (typecheck) else throw
:
try {
doSomethingA();
doSomethingB();
doSomethingC();
} catch (e: string) {
console.error(e);
} catch (e: number) {
console.error('example error message:', e);
} catch (e: Error) {
console.error(e.message);
}
this could then be transformed into:
try {
doSomethingA();
doSomethingB();
doSomethingC();
} catch (e) {
if (typeof e === 'string') {
console.error(e);
} else if (typeof e === 'number') {
console.error('example error message:', e);
} else if (e instanceof Error) {
console.error(e.message);
} else {
throw e;
}
}
@zzzzBov
How about doing something similar except using instanceof
? That way, it could mimic aspects of other languages that folks may be used to. If so, it would be nice if TS could at least warn if ordering of catch clauses caused one clause to become unreachable because a clause with a more general type is ahead of it.
@treynash
How about doing something similar except using
instanceof
?
I don't fully understand what you mean here. Are you saying it should not use instanceof
at all or that it should only use instanceof
?
it would be nice if TS could at least warn if ordering of catch clauses...
I definitely agree that a warning for unreachable code would be useful, however I'm not sure if that necessarily belongs in TS or whether that task should be delegated to a linter. I don't feel strongly one way or the other 🤷♂.
@zzzzBov
I should have been more precise. What I meant was that instead of if (typeof e === 'Foo')
, it could be something like if( e instanceof Foo)
. That way, it should match anything that extends Foo
as well as instances of Foo
, which is what I meant by when I referenced the way some other languages work.
@treynash ah ok, Then I should point out that my example uses instanceof
for Error
. It _can't_ use instanceof
for String
or Number
types because primitive values are not instances.
@zzzzBov
Yep, indeed. And I would imagine one would also want to be able to discern based on things like union and intersection types.
It's more verbose, but what about a syntax like c#'s exception filters: catch (identifier) when (expression) { statements; }
, where identifier
is in-scope in expression
, and the same type guard semantics apply as in a regular if
statement.
Maybe at that point you might as well just stick an if statement in your regular catch block, I suppose
Now that ESLint 7 is out with a new set of no-unsafe-*
and the restrict-template-expressions
rules, the untyped catch clauses make the lint output extremely noisy. The following short snippet contains a whopping 2 ESLint errors just because we are not easily able to assert that e
is indeed an Error
:
try {
throw new Error("nope");
} catch (e) {
console.log(`Had error: ${e.message}`);
// ^-- Two lint errors here:
// Invalid type "any" of template literal expression. eslint [@typescript-eslint/restrict-template-expressions]
// Unsafe member access .message on an any value. eslint [@typescript-eslint/no-unsafe-member-access]
}
I'm fine with allowing any
in template literals, but the no-unsafe-member-access
rule is actually very helpful. Since ESLint also makes sure that no non-Errors are thrown, I do like @DanielRosenwasser's idea of casting explicitly (catch (e as Error)
).
Up .message should be available when you're in catch clause
I can't believe this is still an open issue. Seems like a very obvious feature for TypeScript. Can't wait to see it!
I personally prefer catch(error as Error)
as opposed to catch(error: Error)
because the first makes more sense because you're casting and faking it, but if we used the second method then people may mistakenly believe we are telling the try-catch block to behave in a certain way which is not what TypeScript is for. That should be a new JavaScript feature.
Still, @zzzzBov did show an interesting way of getting around this which is something I'm completely up for.
Now that ESLint 7 is out with a new set of
no-unsafe-*
and therestrict-template-expressions
rules, the untyped catch clauses make the lint output extremely noisy. The following short snippet contains a whopping 2 ESLint errors just because we are not easily able to assert thate
is indeed anError
:try { throw new Error("nope"); } catch (e) { console.log(`Had error: ${e.message}`); // ^-- Two lint errors here: // Invalid type "any" of template literal expression. eslint [@typescript-eslint/restrict-template-expressions] // Unsafe member access .message on an any value. eslint [@typescript-eslint/no-unsafe-member-access] }
I'm fine with allowing
any
in template literals, but theno-unsafe-member-access
rule is actually very helpful. Since ESLint also makes sure that no non-Errors are thrown, I do like @DanielRosenwasser's idea of casting explicitly (catch (e as Error)
).
My ugly workaround for the new unsafe-* rules is just
catch (ex) {
const error = ex as Error;
logger.info('my error is meh', { error });
logger.debug(error.message);
I prefer this over disabling the rule for the line/file. still meh.
This is more-or-less addressed in https://github.com/microsoft/TypeScript/pull/39015, which will land in 4.0. Not perfect, but better than nothing.
@guidobouman I agree that #39015 is a small step towards this feature but in no way addresses it. That merge request is mostly does nothing to address #20024 IMO because it requires unknown
or any
. Even if that merge request did allow for types, it wouldn't solve this issue unless it caught only those types in the compiler.
Something like this is what we're looking for:
try {
doSomethingA();
doSomethingB();
doSomethingC();
} catch (e) {
if (typeof e === 'string') {
console.error(e);
} else if (typeof e === 'number') {
console.error('example error message:', e);
} else if (e instanceof Error) {
console.error(e.message);
} else {
throw e;
}
}
Right now it's a bit verbose. What if I want to catch some exceptions and have it to continue to throw if it's not the one I wanted?
Currently I have to:
try {
something();
} catch (err) {
if (err instanceof SomeError) {
doSomethingElse();
} else if (err instanceof AnotherError) {
doAnotherThing();
} else if (err instanceof YetAnotherError) {
yetAnotherThing();
} else {
throw err;
} // Else //
} // Try, Catch //
What I wouldn't mind seeing is:
try {
something();
} catch (err: SomeError) {
doSomethingElse();
} catch (err: AnotherError) {
doAnotherThing();
} catch (err: YetAnotherError) {
yetAnotherThing();
} // Try, Catch //
Which would just output what was above. Would save me some typing and be pretty convenient like in Java and other languages.
Multiple catches are not a pattern that is known to JavaScript. Feels weird to change the structure of the language.
Multiple catches are not a pattern that is known to JavaScript. Feels weird to change the structure of the language.
Neither is private/public constructor fields, or public/protected/private class fields, etc etc.. Need I go on?
Neither is private/public constructor fields, or public/protected/private class fields, etc etc.. Need I go on?
Microsoft did those features in the earlier days of TypeScript, but not long after decided to more closely align with the ECMAScript specification, which they have done extremely well since. Their design goals are clear and they will not be introducing features like this that deviate from the specification.
The whole point of Typescript is to type check as a transpiler to JavaScript, which is why we're not using vanilla JavaScript.
If you feel a feature could benefit many others, but dismiss it because it "feels weird" but makes sense in other languages and is successful and many use those features, why do you raise your voice against it?
We use Typescript for the sugar that it provides. Decorators? Typed variables? Type checks? Generics? Function overloads? Don't forget the new variadic tuple types in typescript 4.
The point is, it's meant for a more Java or OOP programming that helps you have more typed and safe control over your code. It will always run JavaScript, that's a given (as I think they stated that as a mission goal).
But we shouldn't feel weird when using a language that transpiles into another language if there's differences.. there will always be differences, and that's good.. Typescript !== JavaScript.
but not long after decided to more closely align with the ECMAScript specification
Can you provide a source reference to this decision? I can't seem to find it.
I'd like to be a part of that discussion. That's why I'm asking where you found it.
I don’t use TypeScript for the sugar it provides. The sugar is the very thing that is not in their design goals today. It’s more about confidence and type safety and all the type information disappears at compile time, as it should. Decorators are another one of the earlier features they implemented before ironing out the design goals, so it’s irrelevant to your argument.
I don’t think you’re going to get any traction requesting to change the design goals, BTW.
Can you provide a source reference to this decision? I can't seem to find it.
See Goals here: https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals
Can you provide a source reference to this decision? I can't seem to find it.
See Goals here: https://github.com/microsoft/TypeScript/wiki/TypeScript-Design-Goals
How would the multiple catch clause go against the mission goals exactly?
Also, they're aligning "with current and future ECMAScript proposals", not with the specification itself. There's a difference.
And I'm not requesting to change the design goals.
And again, I was asking for the reference to the decision to do this, as was mentioned. The commenter said there was a decision and I only would like to know where it is. :)
they will not be introducing features like this that deviate from the specification.
Are you speaking on behalf of Microsoft? Are you on the Microsoft team? Who's specification? JavaScript? Well introduce types and immediately it's against JavaScript specification.. what's your point?
In fact, I think this "aligns" with goal #1 very nicely.. "1. Statically identify constructs that are likely to be errors."
By clarifying in more detail to exactly catch the different typed errors visually and cleaner, than using many if statements in one single catch block, and safely rethrow if none are found.. just like others are most likely used to coming from java and the like.
I’ve been following the project since the beginning and I’ve seen and even started enough of these types of conversations to know where Microsoft stands on these points. I didn’t like it at first, but now I 100% get it and respect their design goals.
There are other ways to statically identify constructs that are likely to be errors without adding new JS syntax that requires not just throwing away type information at compile time, but also modifying the AST.
I think the best way to think about it is if you throw away all the type information, the JS that is left should ideally be runnable, which might also require shims, but runnable without modification to the AST.
Again, you seem to think this is against the design goals? Many changes in the language including types, change the AST (Abstract Syntax Tree). Have you ever worked on a compiler? I have (if anyone is curious I did a bit of work on the Free Pascal compiler).
I referenced what the JavaScript could would be here: https://github.com/microsoft/TypeScript/issues/20024#issuecomment-671116935
What the transpiler would emit. It's safe, simple, straight forward. Requires no shims, and does not somehow modify JavaScripts AST. :)
The first non-goal states the following:
use the behavior of JavaScript and the intentions of program authors as a guide for what makes the most sense in the language.
If this were Python, your request here would make perfect sense, as Python already supports it like so:
try:
print(x)
except NameError:
print("Variable x is not defined")
except:
print("Something else went wrong")
But this is not Python.
Non-goal #5 states the following:
emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.
You can see the problem pretty quickly if you try to transpile your proposal into JS. My best attempt would be the following:
try {
something();
} catch (err) {
if (err istanceof SomeError) {
doSomethingElse();
} else if (err instanceof AnotherError) {
doAnotherThing();
} else if (err instanceof YetAnotherError) {
yetAnotherThing();
}
}
Notice how this now "requires runtime metadata"; furthermore, what should happen if SomeError
were not a class, but an interface? What then? TS is _not_ going to be doing run-time type checking. Remember that it's not just errors that can be caught, as you can throw
anything.
Again, you seem to think this is against the design goals?
It's against the non-goals, as stated above. Also, you're looking for "sugar," which is not in the design goals.
Many changes in the language including types, change the AST (Abstract Syntax Tree).
Sure, but my point was "after the types have been removed."
Have you ever worked on a compiler?
I translated the entire PostCSS project into TypeScript, but I don't see how that's relevant. I've also used the TypeScript compiler API to manipulate and create AST, so I'm very aware of how it works.
What the transpiler would emit. It's safe, simple, straight forward. Requires no shims, and does not somehow modify JavaScripts AST. :)
It's only simple in your exact use case and only if all the types are classes or functions (instanceof-able). It also modifies the AST, for sure. I'm not sure why you think that it doesn't, as it has to translate catch clauses into separate if
statements with instanceof
checks. You are deleting the catch clause AST nodes and inserting new ones for the if
statements.
Notice how this now "requires runtime metadata"
It requires no runtime metadata, as you can see. :)
I asked if you worked on a compiler because you mentioned it modified the AST, which most real typescript language changes do? Arrow functions in fact translate to "function ()" and surrounds by an enclosure if I'm not mistaken? Either way, features like that drastically change the emitted AST.. Like most of the features when they target ES5.
Anyway, I'm done debating on this. If others would like it, they can voice.
Notice how this now "requires runtime metadata"
It requires no runtime metadata, as you can see. :)
It absolutely does. The instanceof
checks are runtime checks. And there is no way to check these statements against an interface or type that is not a class or function.
I asked if you worked on a compiler because you mentioned it modified the AST, which most real typescript language changes do? Arrow functions in fact translate to "function ()" and surrounds by an enclosure if I'm not mistaken?
Only to support older targets, but you are mistaken if you are talking about compiling to a modern target. Aside from access modifiers and decorators, you should be able to compile ESNext
TS into ESNext
JS by simply removing the type nodes from the AST.
Have you ever worked on a compiler? I have (if anyone is curious I did a bit of work on the Free Pascal compiler).
Hey all, let's please be respectful in tone - this can come off as intimidating.
@cyraid Even if this is a syntactically-driven construct (i.e. only entities that resolve to both a value and a type are allowed in those catch
clauses),
: EntityName
for anything other than erasable type annotations that have no runtime behaviorOn TypeScript, we intentionally aim not to add new constructs that have any sort of runtime behavior at all if they are not part of ECMAScript, and that stance has hardened over the years.
Sorry, my answer was a bit short. This discussion has exploded a bit, but let me explain my initial reasoning: Sure, they (most typescript features) are additions, but not structurally. Most annotations can be removed without transpilation. I'm a fan of terser forms of expressing a goal. But not when it changes the mental flow of the language, and might possiby collide with the development of EcmaScript itself. That's also reflected in their design goals, particularly:
- Avoid adding expression-level syntax.
See https://github.com/microsoft/TypeScript/issues/18500#issuecomment-329834072 as a reference.
I wonder when will this issues be implemented?
Most helpful comment
I think a better solution would be to allow typed
catch
blocks to be transpiled toif (typecheck) else throw
:this could then be transformed into: