18

I stumbled upon the possibility to add a cause to the Error constructor in javascript.

But when I try to use this feature my app does not start as it does not know this "new" constructor arg.

> tsc && node dist/index.js
promo/promo-service/am-promo-request-handler.ts:43:104 - error TS2554: Expected 0-1 arguments, but got 2.        
43         throw new Error(`Can't read Maxmind GeoLite2 City db from mmdb file '${config.pathMmdbCity}'`, { cause: err});
Found 1 error in promo/promo-service/am-promo-request-handler.ts:43 

All of the following commands stop with the above compile error

nodemon
tsc && node dist/index.js
ts-node index.ts

I added the following script to my package.json (to be sure to ask the right instance of node and the other tools for its version)

"check": "nodemon -v && node -v && tsc -v && ts-node -v && npm -v"

It returns

2.0.19
v16.14.2
Version 4.7.4
v10.9.1
8.17.0

The feature should be available since node version 10.9.0

@types/node@18.7.6

My tsconfig.json

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist/",
    "removeComments": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}

I'm using VSC on Windows.

Any idea what I need to change/update?

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
andymel
  • 4,538
  • 2
  • 23
  • 35
  • 3
    It's `tsc` which can't handle it, not `node` – Konrad Aug 16 '22 at 18:05
  • @KonradLinkowski: true, but still the reason it just doesn't work in tsc but works in plain node is worth of explaining. – Wiktor Zychla Aug 16 '22 at 18:14
  • Not a direct solution, but you can try this https://www.npmjs.com/package/@types/error-cause – Konrad Aug 16 '22 at 18:29
  • @KonradLinkowski thanks, I already found that one. But I guess this should also work without an additional types module?! – andymel Aug 16 '22 at 19:51
  • [^](https://stackoverflow.com/questions/73378375/cant-use-new-features-of-nodejs#comment129586189_73378375) TypeScript handles the `cause` property with no problems (https://tsplay.dev/w2364N). It's likely an issue with your configuration somewhere. (Note that the type of `Error.cause` will be changing to `unknown` in TS v[`4.8`](https://devblogs.microsoft.com/typescript/announcing-typescript-4-8-rc/#lib-d-ts-updates).) – jsejcksn Aug 16 '22 at 20:03
  • By the way it's `throw new Error` and not `throw Error`. – CherryDT Aug 16 '22 at 20:04
  • Thanks @CherryDT, that was just a copy paste error. The problem is Independent of that. – andymel Aug 16 '22 at 20:08
  • @jsejcksn I have added my tsconfig.json above. Is there anything suspicious? – andymel Aug 16 '22 at 20:19
  • @andymel Can you show us the unmodified output after running `tsc`? The error you initially provided is from `ts-node` and they won't be identical. – jsejcksn Aug 16 '22 at 20:24
  • @jsejcksn have replaced my shortened tsc output with the full output above – andymel Aug 16 '22 at 20:36
  • This is your Typescript definitions not understanding that the Error object constructor can take two arguments as it says it is expecting 0 or 1 argument. You need updated Typescript definitions. – jfriend00 Aug 17 '22 at 03:30
  • 1
    @jfriend00 Do you have an idea what exactly I have to update? (see current versions above, I guess `@types/node` is relevant... it's at `18.7.6`). When I follow the Error constructor to its code via VSC, it leads me to `...AppData\Local\Programs\Microsoft VS Code\resources\app\extensions\node_modules\typescript\lib\lib.es5.d.ts` where the constructor has NO options object with a cause parameter. The target in my `tsconfig.json` is not es5. Maybe the tsconfig.json is not used? – andymel Aug 17 '22 at 11:27
  • Things to check. Do you have the latest version of TypeScript installed? Are you configuring a target environment that is pretty new (so it will be expecting the Error constructor to support two arguments)? What is that target environment? Is it using your tsconfig.json file? – jfriend00 Aug 17 '22 at 16:01
  • 1
    Why are you using `"target": "es2017"`? That's five years old and will not support an Error constructor with the `options.cause` parameter. – jfriend00 Aug 17 '22 at 16:02
  • 1
    FYI, Nodejs just started supporting the options argument for the Error constructor in Sept 2021. – jfriend00 Aug 17 '22 at 16:06
  • Thanks a lot for the help. Embarrassing, but I just learned now what ES versions exist and how 'target' and 'lib' work. Now, everything is clear. I think just took the latest ES.. that I saw on some page (that was obviously not complete) – andymel Aug 18 '22 at 10:47

2 Answers2

23

Note: I'll ignore the nodemon and ts-node aspects of your question details because they are additional layers of complexity and not related to the core problem.

Because you haven't shown your code, I'll provide a (contrived) example for this answer that both accesses the cause property of an error instance and uses it in construction to demonstrate the issue with your configuration and explain what's wrong and how to fix it:

Example code and initial configuration

Note: you can manually copy all of the example files in this answer and save them on your storage device to follow along, or you can just copy this script and paste it into your browser's console to download the entire example project as a zip archive:

(() => {
  function createBase64DataUrl (mediaType, b64Str) {
    return `data:${mediaType};base64,${b64Str}`;
  }

  function download (url, fileName) {
    const a = document.createElement("a");
    a.href = url;
    a.download = fileName;
    a.click();
    a.remove();
  }

  const zipArchiveData = "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";
  const dataUrl = createBase64DataUrl("application/zip", zipArchiveData);
  download(dataUrl, "so-73378375.zip");
})();

Let's get started. Here's an example TypeScript module:

./src/module.ts:

import {
  AssertionError,
  equal as assertEqual,
  ok as assert,
} from 'node:assert/strict';

/** Find the innermost nested cause */
function getRootException (exception: unknown): unknown {
  let root = exception;
  let current = exception;
  while (current instanceof Error) {
    current = current.cause;
    if (typeof current !== 'undefined') root = current;
  }
  return root;
}

function getQuotedReason (exception: unknown): string {
  const rootException = getRootException(exception);

  // Get a reason string, preferring an error message
  // if the value is an Error instance:
  const reasonStr = rootException instanceof Error
    ? rootException.message
    : String(rootException);

  // Return a quoted version of the string:
  return JSON.stringify(reasonStr);
}

function example () {
  try {
    throw new Error(`I don't have a cause`);
  }
  catch (ex) {
    console.log(`The final reason was: ${getQuotedReason(ex)}`);
  }

  try {
    const cause = new Error(`I'm the final error cause`);
    throw new Error(`It's not my fault!`, {cause});
  }
  catch (ex) {
    console.log(`The final reason was: ${getQuotedReason(ex)}`);
  }

  try {
    const nestedCause = new Error(
      `I'm the cause of the error that caused the top-level error`,
    );

    const cause = new Error(
      `I'm the cause of the top-level error`,
      {cause: nestedCause},
    );

    throw new Error(`I'm the top-level error`, {cause});
  }
  catch (ex) {
    console.log(`The final reason was: ${getQuotedReason(ex)}`);
  }

  try {
    assertEqual(true, false, `True isn't false`);
  }
  catch (ex) {
    assert(ex instanceof AssertionError, 'Expeted an AssertionError');
    assert(ex.message === `True isn't false`);
    console.log('Encountered the expected AssertionError');
  }
}

example();

Here, I'm using the TSConfig shown in your question, and I've just added the include top-level option because the module above (all of the TS source code in this example) is in the ./src directory:

./tsconfig.json:

{
  "compilerOptions": {
    "target": "es2017",
    "module": "commonjs",
    "sourceMap": true,
    "outDir": "./dist/",
    "removeComments": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

And, for completeness, here's my npm package file:

./package.json:

{
  "name": "so-73378375",
  "version": "0.1.0",
  "scripts": {
    "compile": "tsc",
    "example": "npm run compile && node dist/module.js"
  },
  "license": "MIT",
  "devDependencies": {
    "@types/node": "^18.7.6",
    "typescript": "^4.7.4"
  }
}

Also, note that I'm using the latest LTS version of Node at the time of this answer (it's slightly newer than yours, but still 16.x):

% node --version
v16.17.0

If you're following along, now's a good time to npm install:

% npm install

added 2 packages, and audited 3 packages in 1s

found 0 vulnerabilities

Determining the "cause" of your issue

Let's start out by looking at your TSConfig: you've set compilerOptions.target to "es2017". I'm not sure what influenced this decision, but the TypeScript GitHub repo wiki provides Recommended Node TSConfig settings that you can use for guidance:

Node 16

{
  "compilerOptions": {
    "lib": ["ES2021"],
    "module": "commonjs",
    "target": "ES2021"
  }
}

There are also base configs for various environments here.

Some observations:

The lib option is set in addition to target. The documentation for compilerOptions.lib in the TSConfig reference includes this information:

TypeScript also includes APIs for newer JS features matching the target you specify; for example the definition for Map is available if target is ES6 or newer.

What this means is that (according to the way that TS resolves configuration right now) not setting lib in your config is the same as specifying an array with a single value equal to your target. That means we can update your TSConfig's compilerOptions with these values and get the same behavior:

{
  "compilerOptions": {
    "lib": ["es2017"],
    "target": "es2017",
    // --- snip ---
  },
  // --- snip ---
}

We're not done making changes yet (we still need to change the actual lib and target values), but let's run the example script now to see what the compiler tells us:

% npm run example

> so-73378375@0.1.0 example
> npm run compile && node dist/module.js


> so-73378375@0.1.0 compile
> tsc

src/module.ts:12:23 - error TS2550: Property 'cause' does not exist on type 'Error'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2022' or later.

12     current = current.cause;
                         ~~~~~

src/module.ts:41:43 - error TS2554: Expected 0-1 arguments, but got 2.

41     throw new Error(`It's not my fault!`, {cause});
                                             ~~~~~~~

src/module.ts:54:7 - error TS2554: Expected 0-1 arguments, but got 2.

54       {cause: nestedCause},
         ~~~~~~~~~~~~~~~~~~~~

src/module.ts:57:48 - error TS2554: Expected 0-1 arguments, but got 2.

57     throw new Error(`I'm the top-level error`, {cause});
                                                  ~~~~~~~


Found 4 errors in the same file, starting at: src/module.ts:12

The last 3 compilation errors look very much like the one you described in your question. Now that we've reproduced your issue, let's adjust the lib and target values to the one suggested by the TS wiki "ES2021" — note that the capitalization for this value doesn't matter (e.g. "ES2021" is the same as "es2021"):

{
  "compilerOptions": {
    "lib": ["es2021"],
    "target": "es2021",
    // --- snip ---
  },
  // --- snip ---
}

Now that we've changed those values, let's try again:

% npm run example

> so-73378375@0.1.0 example
> npm run compile && node dist/module.js


> so-73378375@0.1.0 compile
> tsc

src/module.ts:12:23 - error TS2550: Property 'cause' does not exist on type 'Error'. Do you need to change your target library? Try changing the 'lib' compiler option to 'es2022' or later.

12     current = current.cause;
                         ~~~~~

src/module.ts:41:43 - error TS2554: Expected 0-1 arguments, but got 2.

41     throw new Error(`It's not my fault!`, {cause});
                                             ~~~~~~~

src/module.ts:54:7 - error TS2554: Expected 0-1 arguments, but got 2.

54       {cause: nestedCause},
         ~~~~~~~~~~~~~~~~~~~~

src/module.ts:57:48 - error TS2554: Expected 0-1 arguments, but got 2.

57     throw new Error(`I'm the top-level error`, {cause});
                                                  ~~~~~~~


Found 4 errors in the same file, starting at: src/module.ts:12

Still the same 4 compilation errors! However, there's a useful suggestion in there after the first error (it was also there the first time we ran it):

Try changing the 'lib' compiler option to 'es2022' or later.

Arriving at the solution

If we examine the core TS library declaration files for ES features (in v4.7.4), we find that the types related to the error cause property are located in lib.es2022.error.d.ts. These types are used in lib.es2022.d.ts and later versions, but are actually not used in lib.es2021.d.ts.

Therefore, we need to set the lib option to at least "es2022".

Is the TS wiki recommendation outdated? Sort of (it's not exactly that simple). Node is always evolving, and that recommendation was likely written for the first release of Node 16. However, support for the Error.cause property was added to Node in release version 16.9.0 on 2021-09-07.

There's also another useful site that tracks Node.js ES feature support: https://node.green.

We only need to update the compilerOptions.lib value, and we can actually set it to anything later than "es2022" (e.g. "esnext"), and TS will still use the compilerOptions.target value to appropriately downlevel incompatible language features. Here's a snippet from the documentation:

The target setting changes which JS features are downleveled and which are left intact. For example, an arrow function () => this will be turned into an equivalent function expression if target is ES5 or lower.

Changing target also changes the default value of lib. You may “mix and match” target and lib settings as desired, but you could just set target for convenience [if newer language features aren't needed in your source code].

That last bracketed clause is mine (not in the docs) and describes the exception of your case.

Let's actually update the lib value now:

{
  "compilerOptions": {
    "lib": ["es2022"],
    // --- snip ---
  },
  // --- snip ---
}

and run the example one more time:

% npm run example

> so-73378375@0.1.0 example
> npm run compile && node dist/module.js


> so-73378375@0.1.0 compile
> tsc

The final reason was: "I don't have a cause"
The final reason was: "I'm the final error cause"
The final reason was: "I'm the cause of the error that caused the top-level error"
Encountered the expected AssertionError

Compilation succeeds, so the program is run and we get the expected output — great!

If you're reading this — thanks for following along. I hope that clarifies everything for you.

jsejcksn
  • 27,667
  • 4
  • 38
  • 62
  • 1
    Finally (unrelated to your Q, so I'm putting this in a comment): I suggest using [ES modules](https://nodejs.org/docs/latest-v16.x/api/esm.html) unless you have an actual need for [CommonJS](https://nodejs.org/docs/latest-v16.x/api/modules.html). CJS is a relic of the past when JavaScript's module system (ESM) was not yet standardized. Following standards is useful for many reasons, and the future of the language is ESM. CJS makes static code analysis difficult/impossible and reduces the potential portability of your code amongst other things. – jsejcksn Aug 17 '22 at 20:34
  • Thanks a lot for the extremely detailed answer. accepted – andymel Aug 18 '22 at 10:52
  • 1
    I honestly wish I could +1 this answer more than once. Excellent explanation and answer @jsejcksn – jamesmhaley Jan 11 '23 at 16:46
  • 1
    If anything needed a TL;DR.... – AnilRedshift Jun 08 '23 at 23:04
  • Excellent answer. One more note: you might find yourself in a position where even setting the right libs doesn't affect your .ts files. Make sure that the file you're looking at is not excluded via the `exclude` field in your tsconfig. I just spend a good while pondering why `{ cause: ... }` didn't work on my error constructors while working on my test files which I had excluded. – kleinfreund Jun 18 '23 at 14:06
10

Short answer: cause is only supported in ES2022 and up. You can tell TypeScript to accept it one of two ways via the compilerOptions in tsconfig.json:

Option 1: Update your target and/or lib to ES2022

Works if you are able to ignore support for older browsers or polyfill them.

{
  "compilerOptions": {
    "target": "es2022",
    "lib": ["es2022"],
  },
}
Option 2: Add just the relevant es2022.error support to lib

This is a better option if you aren't ready to bump your overall target, and instead just want to include specific well-supported features.

{
  "compilerOptions": {
    "target": "es2019",
    "lib": ["es2019", "es2022.error"],
  },
}

Want more nitty gritty detail? Refer to @jsejcksn’s very thorough answer.

coreyward
  • 77,547
  • 20
  • 137
  • 166