@ncjamieson

Catching Unknowns

July 14, 2020 • 4 minute read

Parrot
Photo by Ignacio Amenábar on Unsplash

TypeScript version 4 is going to introduce a bunch of new features and one of those will be allowing unknown to be specified as the type for catch-clause variables.

At the moment, catch-clause variables are implicitly typed as any, so — as far as the compiler is concerned — you can do whatever you want with them:

try {
  /* ... */
} catch (error) {
  console.error(error.message); // YOLO
}

This isn’t changing in version 4 — catch-clause variables will still be implicitly typed as any — but it might change in the future:

While the types of catch variables won’t change by default, we might consider a new --strict mode flag in the future so that users can opt in to this behavior.

In the meantime, catch clauses can be made type-safe by explicitly specifying unknown as the type:

try {
  /* ... */
} catch (error: unknown) {
  console.error(error.message); // error TS2571: Object is of type 'unknown'.
}

Here, with the catch-clause variable typed as unknown, a type guard is necessary to establish whether or not the caught value is actually an Error instance:

try {
  /* ... */
} catch (error: unknown) {
  if (error instanceof Error) {
    console.error(error.message); // It's an Error instance.
  } else {
    console.error("🤷‍♂️"); // Who knows?
  }
}

unknown and any are the only allowed types for catch-clause variables. There is no mechanism for constraining errors that could be thrown from within the try block, so specifying a type narrower than unknown or any would be unsafe.

To encourage type-safe catch clauses, a new rule is being added to the TypeScript ESLint rules: no-implicit-any-catch.

It will effect lint failures for implicit-any catch clauses and will suggest using an explicit unknown. Enabling it will highlight any unsafe assumptions that are being made within your catch blocks.

However, the problem of errors being typed as any isn’t limited to catch blocks. There are two other situations where potentially unsafe code could be lurking.

With a Promise, the rejection callback’s error parameter is typed as any:

somePromise.catch((error) => {
  console.error(error.message); // YOLO
});

And with an RxJS Observable, the catchError operator’s function receives an error parameter that is typed as any:

someObservable.pipe(
  catchError((error) => {
    console.error(error.message); // YOLO
  })
);

In both of these situations, unsafe assumptions would effect errors if the error parameters were typed as unknown, like this:

somePromise.catch((error: unknown) => {
  console.error(error.message); // error TS2571: Object is of type 'unknown'.
});

I’ve added a no-implicit-any-catch rule to eslint-plugin-etc to ensure that Promise rejections have error parameters typed as unknown and a no-implicit-any-catch rule to eslint-plugin-rxjs to do the same for catchError operators.

The rules have suggestions and fixers for automatically updating code to ensure unknown is used as the type. The rules will also ensure that an error parameter’s type is not unsafely narrowed — as with catch clauses, there is no constraint on what could be thrown as an error, so specifying a narrower type would be unsafe.

I’ve also added TSLint rules for devs who’ve not yet made the switch to ESLint. There is an rxjs-no-implicit-any-catch rule in rxjs-tslint-rules that supports catchError and there is a no-implicit-any-catch rule in tslint-etc that supports both Promise rejections and catch clauses.

My thanks go to Felix Becker for suggesting the rules and their behaviour.


Personal blog by Nicholas Jamieson.
Mostly articles about RxJS, TypeScript and React.
GitHubTwitter

© 2020 Nicholas Jamieson All Rights ReservedRSS