Description
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.
Activity
DanielRosenwasser commentedon Nov 15, 2017
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.yortus commentedon Nov 15, 2017
I agree, it is possible to safely annotate the error variable in many situations. Older discussion in #8677 (comment).
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.
aluanhaddad commentedon Nov 20, 2017
@jaredru code like
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.jaredru commentedon Nov 21, 2017
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 isError
. A simple search through the TypeScript repo itself shows thatcatch (e)
virtually always assumes thee
to be anError
. 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.aluanhaddad commentedon Nov 21, 2017
I don't think that is a good pattern either.
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 thecatch
causing an unexpected failure.jaredru commentedon Nov 21, 2017
Sorry, consider it a typo.
<Foo>JSON.parse(s)
is a common pattern but is as unsafe as.catch((e: Error) => {
. Both treat anany
as another type with no guarantee of correctness beyond the developer's word.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 tocatch
to be anError
. This expectation is common in real world codebases--including, but certainly not limited to, TypeScript's.felixfbecker commentedon Nov 28, 2017
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 isError
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 withany
unknown
and narrow it down manuallyAnd 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?
jaredru commentedon Nov 28, 2017
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
(ornull
, or a string, ...) and is with almost certainty anError
. I would simply like the compiler to help me out in this case without additional code or runtime overhead.lordazzi commentedon Jun 1, 2018
TypeScript language is already the best I've ever worked.
If you type the catch clause this will make much better.
ghost commentedon Aug 31, 2018
I'd like to at least be able to do
84 remaining items
catch
with automaticinstanceof
checks #46690octogonz commentedon Feb 11, 2022
@bodograumann As of TypeScript 4.4 you can set
useUnknownInCatchVariables=false
which restores the oldany
behavior. It's not ideal, but nobody was complaining about this in TypeScript 3.x. (In fact the TypeScript compiler itself is currently building this way -- if you setuseUnknownInCatchVariables=true
insrc/tsconfig-base.json
, the build fails.)useAuth
to handle more auth hub events aws-amplify/amplify-ui#2795useErrorInCatchVariables
#51390spephton commentedon Apr 29, 2023
A nice way to safely unwrap your
unknown
error is to useinstanceof
(as mentioned upthread)You're explicitly handling all possibilities, so it's type-safe.
I think catch variable as unknown is a good default as it encourages you to think about the values that variable can take.
It may make sense to have a tsconfig flag to allow
catch (e: Error)
to ease the pain for some users however.dimikot commentedon Jan 30, 2024
Another (type safe) option is to allow to safely cast to something like:
where MaybeError is some custom type defined similar to:
I.e. allow
e: MaybeError
if MaybeError is null, undefined, or is an object with all props being optional and of typesunknown
. In this case, it still allows to do lots of things (checking for props being present, checking for equality likee?.code === "some"
etc.), but at the same time, does not compromise type safety.This would work, because, when MaybeError is defined the above way, there is no JS thing which would not match it. Even null or a plain string or a number thrown will still match.