88

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?

0

5 Answers 5

98

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.

Sign up to request clarification or add additional context in comments.

2 Comments

Thanks. I guess that's the same reason that Promise.catch() has no type argument. Still, I wish I could write down what I expect the error to be... a lot of TS is just writing what you expect, certainly there's no guarantee any of your types will be true at runtime!
If you do not have available error types, this can translate to catch (e) { if ((e as {code: string}).code === 'YOUR ERROR CODE') { ... } and a cleaner implementation was suggested in Benny Neugebauer's answer
19

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 a 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: unknown): x is ApiError {
  if (x && typeof x === 'object' && 'code' in x) {
    return true;
  }
  return false;
}

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

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

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

function isApiError(x: unknown): x is ApiError {
  if (x && typeof x === 'object' && 'code' in x) {
    return true;
  }
  return false;
}

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

2 Comments

Note that you should probably use unknown instead of any, since any suppresses type errors... unknown forces you to correctly assert that 1) the error is an object 2) it is not null 3) that x.code is a number
@JackSteam totally agree! I updated my example with "unknown" and made a tutorial about it: youtube.com/watch?v=xdQkEn3mx1k
10

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.

Comments

5

A simple solution

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

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

Comments

4

Another option is to validate the error instance, see below:

try {
        
} catch (err) {
  if (err instanceof Error) {
    console.log(err.message);
  }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.