Skip to content

Allow type annotation on catch clause variable #20024

Open
@jaredru

Description

@jaredru

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

DanielRosenwasser commented on Nov 15, 2017

@DanielRosenwasser
Member

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.

added
Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this feature
SuggestionAn idea for TypeScript
on Nov 15, 2017
yortus

yortus commented on Nov 15, 2017

@yortus
Contributor

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

aluanhaddad commented on Nov 20, 2017

@aluanhaddad
Contributor

@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);
jaredru

jaredru commented on Nov 21, 2017

@jaredru
Author

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.

aluanhaddad

aluanhaddad commented on Nov 21, 2017

@aluanhaddad
Contributor

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.

jaredru

jaredru commented on Nov 21, 2017

@jaredru
Author

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.

felixfbecker

felixfbecker commented on Nov 28, 2017

@felixfbecker
Contributor

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

jaredru

jaredru commented on Nov 28, 2017

@jaredru
Author

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.

lordazzi

lordazzi commented on Jun 1, 2018

@lordazzi

TypeScript language is already the best I've ever worked.
If you type the catch clause this will make much better.

ghost

ghost commented on Aug 31, 2018

@ghost

I'd like to at least be able to do

  try {
    await rejected;
  } catch (err: unknown) { 
    // ...
  }

84 remaining items

octogonz

octogonz commented on Feb 11, 2022

@octogonz

This is now a real pain in typescript 4.4 with --strict, where exceptions are typed as unknown instead of any in the catch block.
I‘m not really happy with any solution I have seen

@bodograumann As of TypeScript 4.4 you can set useUnknownInCatchVariables=false which restores the old any 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 set useUnknownInCatchVariables=true in src/tsconfig-base.json, the build fails.)

spephton

spephton commented on Apr 29, 2023

@spephton

A nice way to safely unwrap your unknown error is to use instanceof (as mentioned upthread)

catch (e) {
    if (e instanceof Error) {
       // type-safe, compiler can help you in this block
        console.log(e.message);
    }
    else {
        // optionally
        throw e;
    }
} 

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

dimikot commented on Jan 30, 2024

@dimikot

Another (type safe) option is to allow to safely cast to something like:

} catch (e: MaybeError) {

where MaybeError is some custom type defined similar to:

type MaybeError = null | undefined | { message?: unknown; stack?: unknown; };

I.e. allow e: MaybeError if MaybeError is null, undefined, or is an object with all props being optional and of types unknown. In this case, it still allows to do lots of things (checking for props being present, checking for equality like e?.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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Add a FlagAny problem can be solved by flags, except for the problem of having too many flagsAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      Participants

      @tonycoco@alfaproject@dimikot@guidobouman@bluelovers

      Issue actions

        Allow type annotation on catch clause variable · Issue #20024 · microsoft/TypeScript