87

I have the following code:

try {
  phpDoc(vscode.window.activeTextEditor);
} catch (err) {
  console.error(err);
  vscode.window.showErrorMessage(err.message);
}

however err.message gets the error Object is of type 'unknown'.ts(2571) on err., but I cannot type the object in catch (err: Error).

What should I do?

joshuakcockrell
  • 5,200
  • 2
  • 34
  • 47
Mathias Hillmann
  • 1,537
  • 2
  • 13
  • 25
  • You can use the module `cast-error` to deal with the two main issues (javascript can send any value, and typescript unknowns the error type) with it: https://www.npmjs.com/package/cast-error – Emilio Platzer Oct 23 '21 at 02:34
  • 1
    @EmilioPlatzer when recommending a package/service, it is advised to disclose the affiliation one has with the said package. – Oleg Valter is with Ukraine Oct 23 '21 at 09:56

5 Answers5

87

As a supplementary answer to CertainPerformance's one:

Up until TypeScript 4.0, the catch clause bindings were set to any thus allowing easy access to the message property. This is unsafe because it is not guaranteed that what's thrown will be inheriting from the Error prototype - it just happens that we don't throw anything but errors as best practice:

(() => {
    try {
        const myErr = { code: 42, reason: "the answer" };
        throw myErr; //don't do that in real life
    } catch(err) {
        console.log(err.message); //undefined
    }
})();

TypeScript 4.0 introduced an option for a safer catch clause by allowing you to annotate the parameter as unknown, forcing you to either do an explicit type assertion or, even better, to type guard (which makes the clause both compile-time and runtime-safe).

However, to avoid breaking most of the codebases out there, you had to explicitly opt-in for the new behavior:

(() => {
    try {
        throw new Error("ouch!");
    } catch(err: unknown) {
        console.log(err.message); //Object is of type 'unknown'
    }
})();

TypeScript 4.4 introduced a new compiler option called useUnknownInCatchVariables that makes this behavior mandatory. It is false by default, but if you have the strict option turned on (as you should), it is turned on which is most likely the reason why you got the error in the first place.

  • 3
    so, to fix it do you make `useUnknownInCatchVariables` false again? – theProCoder Nov 15 '21 at 11:44
  • 7
    @theProCoder well, that is one way to approach this. The rest depends on how devout you are to strict type checking: of course, the safest way is to actually perform runtime checks (that double as guards) with `instanceof` (see CertainPerformance's answer), the most "honest" way without runtime taxing is a type assertion (as we really just "assume" the `catch` block parameter is of a certain type), and the most "go away, please" approach is, yes, explicitly setting the `useUnknownInCatchVariables` to false (see dast's answer) – Oleg Valter is with Ukraine Nov 15 '21 at 13:14
  • 1
    For those using VSCode whose error message or red underline won't go away after changing the `useUnknownInCatchVariables` compiler option, try: Cmd+Shift+P > Typescript: Restart TS Server. – thegoodhunter-9115 Sep 27 '22 at 00:24
60

If you don't want to change all your code after upgrading your TypeScript but are in strict mode, you can add the following compiler option after the strict option to overwrite it, as was hinted in Oleg's answer:

tsconfig.json

{
  "compilerOptions": {
    [...]
    "strict": true,
    "useUnknownInCatchVariables": false,
    [...]
    },
  },
}

"strict": true, sets useUnknownInCatchVariables to true, and then "useUnknownInCatchVariables": false, overrides that and sets it back to false.

dast
  • 767
  • 5
  • 7
  • You can't put comments in JSON. – Boris Verkhovskiy Oct 12 '21 at 04:33
  • 1
    @Boris that's both correct and not - I assume the OP is using [JSONC](https://stackoverflow.com/q/14851903/11407695) also known as "JSON with comments" and available with the Visual Studio Code IDE (which is what is often used with TypeScript projects) which makes their code example valid. Still, I am inclined to agree that one can't put comments in just JSON :) – Oleg Valter is with Ukraine Oct 12 '21 at 16:11
  • 2
    @Boris No idea how is that relevant to the actual answer, when its clearly used just for example. – Petar Oct 15 '21 at 13:03
  • @Petar it was relevant to me because I had to delete them after copy and pasting. – Boris Verkhovskiy Oct 15 '21 at 13:34
  • 2
    Comments are allowed in TSconfig.json files, see https://github.com/microsoft/TypeScript/issues/4987 – Cattode Nov 03 '21 at 16:13
49

It's because anything can be thrown, hence unknown.

const fn = () => {
  throw 'foo';
};
try {
  fn();
} catch(e) {
  console.log(e);
  console.log(e instanceof Error);
  console.log(e === 'foo');
}

You'll need to check that the err actually is an error to narrow it down before accessing the message property.

try {
  phpDoc(vscode.window.activeTextEditor);
} catch (err) {
  console.error(err);
  if (err instanceof Error) {
    vscode.window.showErrorMessage(err.message);
  } else {
    // do something else with what was thrown, maybe?
    // vscode.window.showErrorMessage(String(err));
  }
}
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • if I , as an author, have written the function which throws the error, wouldn't it be ok for me to annotate the error with the more specific type? why does typescript stop me from doing that? – gaurav5430 Oct 31 '21 at 10:23
  • 1
    You may *hope* that if your function throws an error, it will throw an error of your noted type, but there's always the chance that it throws a different, unexpected error - so annotating the type of the error (in an abnormal completion) isn't possible. (if you want to be perfectly type-safe, which `strict` tries to implement) – CertainPerformance Oct 31 '21 at 15:01
  • but isn't that true for any function which calls any other function which might throw or may be there is a system exception, we are still able to annotate the return type for such functions – gaurav5430 Oct 31 '21 at 16:26
  • 1
    Yes, the normal return type of any function is easy to annotate - but exceptions, on the other hand, are unpredictable (they could be anything, not just those that you hope for), so their potential "error type" can't be annotated. A function that ends normally can be well-understood in advance by the type-checker. A function that doesn't end normally means that anything could have happened. – CertainPerformance Oct 31 '21 at 16:44
5

You cannot write a specific annotation for the catch clause variable in typescript, this is because in javascript a catch clause will catch any exception that is thrown, not just exceptions of a specified type.

In typescript, if you want to catch just a specific type of exception, you have to catch whatever is thrown, check if it is the type of exception you want to handle, and if not, throw it again.

meaning: make sure the error that is thrown is an axios error first, before doing anything.

solution 1

using type assertion
Use AxiosError to cast error

import  { AxiosError } from 'axios';

try {
    // do some api fetch
    } catch (error) {
    const err = error as AxiosError
    // console.log(err.response?.data)
    if (!err?.response) {
        console.log("No Server Response");
    } else if (err.response?.status === 400) {
        console.log("Missing Username or Password");
    } else {
        console.log("Login Failed");
    }  
}
solution 2

check if the error is an axios error first, before doing anything

import axios from "axios"

try {
    // do something
} catch (err) {
    // check if the error is an axios error
    if (axios.isAxiosError(err)) {
        // console.log(err.response?.data)
        if (!err?.response) {
            console.log("No Server Response");
        } else if (err.response?.status === 400) {
            console.log("Missing Username or Password");
        } else {
            console.log("Login Failed");
        } 
    } 
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
neil
  • 353
  • 1
  • 8
  • 17
2

My TypeScript version is under 4.0, and I can't make it work again, then I created an auxiliar function to normalize the errors, like following:

interface INormalizedError {
  /**
   * Original error.
   */
  err: unknown;

  /**
   * Is error instance?
   */
  isError: boolean;

  /**
   * Error object.
   */
  error?: Error;

  /**
   * Call stack.
   */
  stack?: Error['stack'];

  /**
   * Error message.
   */
  message: string;

  toString(): string;
}

/**
 * Normalize error.
 *
 * @param err Error instance.
 * @returns Normalized error object.
 */
function normalizeError(err: unknown): Readonly<INormalizedError> {
  const result: INormalizedError = {
    err,
    message: '',
    isError: false,
    toString() {
      return this.message;
    }
  };

  if (err instanceof Error) {
    result.error = err;
    result.message = err.message;
    result.stack = err.stack;
    result.isError = true;
    result.toString = () => err.toString();
  } else if (typeof err === 'string') {
    result.error = new Error(err);
    result.message = err;
    result.stack = result.error.stack;
  } else {
    const aErr = err as any;

    if (typeof err === 'object') {
      result.message = aErr?.message ? aErr.message : String(aErr);
      result.toString = () => {
        const m = typeof err.toString === 'function' ? err.toString() : result.message;
        return (m === '[object Object]') ? result.message : m;
      };
    } else if (typeof err === 'function') {
      return normalizeError(err());
    } else {
      result.message = String(`[${typeof err}] ${aErr}`);
    }

    result.error = new Error(result.message);
    result.stack = aErr?.stack ? aErr.stack : result.error.stack;
  }

  return result;
}

An usage example:

try {
  phpDoc(vscode.window.activeTextEditor);
} catch (err) {
  const e = normalizeError(err);
  console.error(err);
  vscode.window.showErrorMessage(e.message);
}
Eduardo Cuomo
  • 17,828
  • 6
  • 117
  • 94