How do you use typed errors in async catch()

Typescript

Typescript Problem Overview


I am using an async function to call an existing promise-based API which rejects the promise with a typed error.

You could mock this behavior like this:

interface ApiError {
  code: number;
  error: string;
}

function api(): Promise<any> {
  return new Promise((resolve, reject) => {
    reject({ code: 123, error: "Error!" });
  });
}

Now with promises, I can annotate the error type to ApiError:

api().catch((error: ApiError) => console.log(error.code, error.message))

But when using async if I try to annotate the error type in try ... catch():

async function test() {
  try {
    return await api();
  } catch (error: ApiError) {
    console.log("error", error);
  }
}

It compiles with error:

> Catch clause variable cannot have a type annotation.

How, then, do I know what kind of error I'm expecting? Do I need to write an assertion in the catch() block? Is that a bug/incomplete feature of async?

Typescript Solutions


Solution 1 - Typescript

In TypeScript, catch clause variables may not have a type annotation (aside from, as of TypeScript 4.0, unknown). This is not specific to async. Here's an explanation from Anders Hejlsberg:

> We don't allow type annotations on catch clauses because there's really no way to know what type an exception will have. You can throw objects of any type and system generated exceptions (such as out of memory exception) can technically happen at any time.

You can check for the existence of error.code and error.message properties (optionally using a user-defined type guard) in the catch body.

Solution 2 - Typescript

You cannot assign a custom type to your error directly but you can use a type guard. A type guard is a function which helps TypeScript's compiler to figure out the type of your error.

Writing a type guard with a type predicate is very easy. You just need to implement a boolean check which accepts any value and figures out if this value has a property of your custom type.

Given your example, I can see that your ApiError has a custom property called code, so a type guard could look like this:

function isApiError(x: any): x is ApiError {
  return typeof x.code === 'number';
}

Note the x is ApiError return type. That's a type predicate!

Using the isApiError function from above, it will be possible now for TypeScript to infer your custom error type:

interface ApiError {
  code: number;
  error: string;
}

async function test() {
  const isApiError = (x: any): x is ApiError => {
    return typeof x.code === 'number';
  };

  try {
    return await api();
  } catch (error) {
    if (isApiError(error)) {
      // Thanks to the type guard, TypeScript knows know what "error" is
      console.log(error.code);
    }
  }
}

You can see a type guard in action here: https://www.youtube.com/watch?v=0GLYiJUBz6k

Solution 3 - Typescript

This error has nothing to do with async. You can't have typed catch variables.

The reason for this is simple: the types in TypeScript only exist until the code is compiled. Once it's compiled, all you've got is typeless JavaScript.

Having type filters on catch clauses would require checking the errors' types at runtime, and there's simply no reliable way to do that, so I would say such a feature is unlikely to ever be supported.

Solution 4 - Typescript

A simple solution

You just need to cast the error type with type assertion.

catch (err) {
  const error = err as CustomErrorType;
  ...
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionAaron BeallView Question on Stackoverflow
Solution 1 - TypescriptSteven BarnettView Answer on Stackoverflow
Solution 2 - TypescriptBenny NeugebauerView Answer on Stackoverflow
Solution 3 - TypescriptJLRisheView Answer on Stackoverflow
Solution 4 - TypescriptMasih JahangiriView Answer on Stackoverflow