TypeScript: Allow `unknown` type annotation on catch clause variable

Search Terms

catch clause unknown exception

Suggestion

Now any kind of type annotation on catch clauses is not allowed. In my understanding, it’s because that it’s unsafe to annotate like catch (err: SpecialError) since any value will be captured actually.

However, due to the type of err is any, it’s also not type safe.

So I suggest to allow annotating unknown , as most safe typing.

(And annotation with any other type won’t be allowed as is)

Examples

try {
  throw 42;
} catch (err) {
  console.error(err.specialFunction()); 
}

can be written safer as

try {
  throw 42;
} catch (err: unknown) {
  if (err instanceof SpecialError) {
    console.error(err.specialFunction()); 
  } else {
    ...
  }
}

Related Issue

#20024

Checklist

My suggestion meets these guidelines:

  • This wouldn’t be a breaking change in existing TypeScript/JavaScript code
  • This wouldn’t change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn’t a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • This feature would agree with the rest of TypeScript’s Design Goals.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 68
  • Comments: 38 (17 by maintainers)

Commits related to this issue

Most upvoted comments

Assuming this is the appropriate place to discuss this, I would like to advocate that the change in 4.0 be either (in order of preference):

  1. catch (error) results in error having a type of unknown
  2. Add a new strict option that makes error be of type unknown
  3. Add a new strict option that requires the user to do catch (error: unknown)

I understand that error is any for legacy reasons (it was before unknown existed) and therefore can’t be trivially changed (at least without a major version bump). However, I would like to see a path that allows us to get away from that eventually, either with a breaking change (error is unknown) or with a new compiler setting that is flipped on with the strict flag.

I would really love this as a compiler option. Being able to set a flag that makes all caught exceptions unknown instead of manually annotating them would be much better, but I guess it would be harder to migrate to.

It should probably also be possible to do:

try {
	// code
} catch (err: any) {
	// error handling
}

In case unknown ever becomes the default inferred type (e.g.: https://github.com/microsoft/TypeScript/issues/27265).

For a 4.0 change (major version update) this certainly feels like it should be included in noImplicitAny. At the moment, I believe this is the last place where implicit any is allowed when noImplicitAny is on, and getting rid of that last place sure would be nice.

Awesome! I’ve made a PR to force adding a unknown or explicit any type to all catch clauses to typescript-eslint: https://github.com/typescript-eslint/typescript-eslint/pull/2202

This thing is only about adding unknown, the discussion about properly adding types to exceptions is should move to #13219.

@hazae41 Sure, but throw error is standard other things are not.

Note that the above adds the ability to add an any/unknown type, but no new flag for its default any not changing existing flags.

I doubt adding checked exception is ever going to work in Typescript, because anytime you call an API you don’t fully control you are adding any to your error type, which makes the whole type any.

I’ve previously tried doing this:

Never use throw. Instead create this simple API:

type CheckedReturn<R, E> = {ok: true, value: R} | {ok: false, value: E}

function Err<E>(e: E): CheckedReturn<any, E> { return {ok: false, value: E} }
function Ok<R>(r: R): CheckedReturn<R, any> { return {ok: true, value: R} }

(optionally add some monadic functions like and_then etc akin to Rust Result)

. Then you always do this:

function foo(...): CheckedReturn<string, SomeError> {
    // ...
    return Ok(actualValue);
    // or
    return Err(errorValue);
}

This sounds great in theory and gives you fully checked exceptions always… Except when you use literally any external or other API, and then you’re back to where you started, except it’s worse: you now have lots of additional effort, two layers of kinds of exceptions and still can get untyped / unchecked exceptions all the time.


The best that can be done is making a union type of all your own possible checked errors (or inherited error classes if you’re okay with not having exhaustive checking / open domain errors):

type OwnErrorType = 
    | { kind: "FetchError", status: 404 | 500, message: string }
    | { kind: "DatabaseConnectionError", reason: pg.ErrorCode }
     // ...

// OR, if you don't care about exhaustiveness, 
// and don't pass your errors through serialization so instanceof does not break

abstract class OwnError extends Error {}
class FetchError extends OwnError {
     constructor(public status: 404 | 500, public message: string) {}
}
// ...

and then on top of each catch block, add a assertion / condition to always instantly narrow your type and rethrow others:

function assertOwnError(err: unknown): asserts err is OwnErrorType {
    /// some check, using instanceof or brands or whatever
   if (!err || !err.__isOwnErrorType) throw err;
   // or: if (err instanceof OwnError)
}

// ...

try {
   // ...
} catch (e: unknown) {
   assertOwnError(e);
   // error is correctly typed here
}

This is then basically the same as catch blocks you expect from other languages:

try {
} catch (OwnError e) {
    // ...
}

I’m already doing this in my own code bases, but to make it ergonomic these things are missing:

  1. Solving this issue: allowing declaring errors as unknown
  2. Eslint-typescript lint that forces always declaring errors as unknown (or strict flag)
  3. Force throwing only of errors of specific types. Possible to do with eslint, needs a rule similar to no-throw-literal

I am excited typescript 4.0 will address this issue.

We had to add a specific --ignore-catch flag to the type-coverage command ( https://github.com/plantain-00/type-coverage#ignore-catch ) to handle the fact that catch {} is the only place an implicit any is mandatory and unavoidable.

Any way to force it to unknown? We have “noImplictAny” checked, which says “Warn on expressions and declarations with an implied ‘any’ type.”

But we get no warning that err is an ‘any’ type.

@hazae41 the go style doesn’t work well in typescript cause the type system doesn’t understand if error is none, the result must be valid. But the rust style is different, typescript do recognize this tagged union pattern therefore you can’t miss the error handing

We’re using ts-results (https://github.com/vultix/ts-results) to use Rust style checked exception in our program. But it is not good enough cause TypeScript doesn’t and won’t have the Rust ? try operator to automatically spread the type to the containing function.

And another concern is that we cant use the checked exception with confidence, because the rest of the world, including the JS builtin library, the npm libraries… doesn’t obey the checked exception way, they throw.

but if there is some additional throw statements we can infer them.

@akxe: It won’t work because the calling function might come from the npm library with a dts declaration. The source code even might not be the JavaScript / TypeScript (e.g. other languages that compile to JavaScript) so that it is impossible to analyze the source code to infer the throws type.

optionally add some monadic functions like and_then etc akin to Rust Result

@phiresky: Hi, please try ts-results, it’s awesome! (I have made a PR to implement into_ok and and_then and not merged yet)

Why not to try infer the possible types of error? They are very well known to the compiler. Isn’t it right that the error is union of all thrown & Error?

function doOrThrow<T>(error: T): true, throws T {
  if (Math.random() > .5) {
    return true;
  } else {
    throw error;
  }
}
try {
  doOrThrow('err1');
  doOrThrow('err2');
  doOrThrow('err3');
} catch (e) { // Type of e = Error | 'err1' | 'err2' | 'err3'.
}

You may ask, why to handle all errors at the same place. For me, the reason was that Express. For every response you can only send headers once (, logically but annoying to handle an error with it when trying to parallel all async tasks).

router.get('path', async (req, res) => {
  try {
    const [part1, part2] = await Promise.all([
       query('SELECT * FROM ...', 'part1 failed'), 
       query('SELECT * FROM ...', 'part2 failed'), 
    ]);
    res.sent({ ...part1, ...part2 });
  } catch (e) { // I would love this err to be infered
    switch (err) {
      case 'part1 failed':
        return res.send(500).send('The first part of data fetching failed');

      case 'part2 failed':
        return res.send(500).send('The second part of data fetching failed');

      default:
        const error: Error = err;
        console.error(err);
        return res.send(500).send('Unknown error');
    }
  }
});

This would be great (together with a lint that forces doing this always).

Right now, catch (e) {} is basically a situation that should be forbidden by noImplicitAny, except it can’t be because you can’t annotate it with any type (except for unknown) since that would be misleading.