@jchip/error 2.1.6 → 2.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # @jchip/error <sup>[![Version Badge][npm-version-svg]][package-url]</sup> <!-- omit in toc -->
1
+ # @jchip/error <!-- omit in toc -->
2
2
 
3
3
  [![License][license-image]][license-url]
4
4
  [![build][build-image]][build-url]
@@ -7,9 +7,11 @@
7
7
 
8
8
  [![npm badge][npm-badge-png]][package-url]
9
9
 
10
- Utilities and polyfill for node.js errors
10
+ - [API reference](https://fynjs.pages.dev/api/modules/_jchip_error) - TypeDoc
11
+ - [Markdown reference](docs/reference.md) - the full API and runtime behavior in one file. Also ships in the package.
12
+ - [GitHub](https://github.com/jchip/fynjs/tree/main/packages/error)
11
13
 
12
- ### API reference: <https://jchip.github.io/error> <!-- omit in toc -->
14
+ Utilities for readable node.js error stacks, and an `AggregateError` subclass that prints nested stacks.
13
15
 
14
16
  # Table of Contents <!-- omit in toc -->
15
17
 
@@ -22,50 +24,49 @@ Utilities and polyfill for node.js errors
22
24
 
23
25
  ### `cleanErrorStack`
24
26
 
25
- ```ts
27
+ ```js
26
28
  import { cleanErrorStack } from "@jchip/error";
27
29
 
30
+ function loadConfig() {
31
+ return JSON.parse("{ bad json");
32
+ }
33
+
28
34
  try {
29
- require("oops");
35
+ loadConfig();
30
36
  } catch (err) {
31
37
  console.log(cleanErrorStack(err));
32
38
  }
33
39
  ```
34
40
 
35
- Output:
41
+ Output, run as `node test/app.mjs` from `/home/me/app`:
36
42
 
37
43
  ```
38
- Error: Cannot find module 'oops'
39
- Require stack:
40
- - /Users/joel/error/test/samples.ts
41
- at Object.<anonymous> (test/samples.js:4:3)
44
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
45
+ at loadConfig (test/app.mjs:4:15)
46
+ at test/app.mjs:8:3
42
47
  ```
43
48
 
44
- vs:
49
+ vs `err.stack`:
45
50
 
46
51
  ```
47
- Error: Cannot find module 'oops'
48
- Require stack:
49
- - /Users/joel/error/test/samples.js
50
- at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
51
- at Function.Module._load (node:internal/modules/cjs/loader:778:27)
52
- at Module.require (node:internal/modules/cjs/loader:1005:19)
53
- at require (node:internal/modules/cjs/helpers:94:18)
54
- at Object.<anonymous> (/Users/joel/error/test/samples.js:4:3)
55
- at Module._compile (node:internal/modules/cjs/loader:1101:14)
56
- at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
57
- at Module.load (node:internal/modules/cjs/loader:981:32)
58
- at Function.Module._load (node:internal/modules/cjs/loader:822:12)
59
- at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
52
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
53
+ at JSON.parse (<anonymous>)
54
+ at loadConfig (file:///home/me/app/test/app.mjs:4:15)
55
+ at file:///home/me/app/test/app.mjs:8:3
56
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
57
+ at async node:internal/modules/esm/loader:647:26
58
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
60
59
  ```
61
60
 
61
+ Frames from `node:` internals are dropped. Paths under the current directory become relative, for both ES module `file://` URLs and CommonJS paths.
62
+
62
63
  ### `aggregateErrorStack`
63
64
 
64
65
  Generate stack with aggregate errors from [AggregateError]
65
66
 
66
67
  Example:
67
68
 
68
- ```ts
69
+ ```js
69
70
  import { aggregateErrorStack, AggregateError } from "@jchip/error";
70
71
 
71
72
  console.log(aggregateErrorStack(new AggregateError([new Error("error 1")], "test")));
@@ -75,78 +76,68 @@ Output:
75
76
 
76
77
  ```
77
78
  AggregateError: test
78
- at Object.<anonymous> (/Users/joel/error/test/samples.js:3:33)
79
- at Module._compile (node:internal/modules/cjs/loader:1101:14)
80
- at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
81
- at Module.load (node:internal/modules/cjs/loader:981:32)
82
- at Function.Module._load (node:internal/modules/cjs/loader:822:12)
83
- at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
84
- at node:internal/main/run_main_module:17:47
79
+ at file:///home/me/app/test/app.mjs:3:33
80
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
81
+ at async node:internal/modules/esm/loader:647:26
82
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
85
83
  Error: error 1
86
- at Object.<anonymous> (/Users/joel/error/test/samples.js:3:53)
87
- at Module._compile (node:internal/modules/cjs/loader:1101:14)
88
- at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
89
- at Module.load (node:internal/modules/cjs/loader:981:32)
90
- at Function.Module._load (node:internal/modules/cjs/loader:822:12)
91
- at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
92
- at node:internal/main/run_main_module:17:47
84
+ at file:///home/me/app/test/app.mjs:3:53
85
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
86
+ at async node:internal/modules/esm/loader:647:26
87
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
93
88
  ```
94
89
 
95
90
  ### `AggregateError`
96
91
 
97
- - Polyfill for [AggregateError]
98
- - node.js 15+ has built-in [AggregateError]
92
+ - Subclass of the built-in [AggregateError]; it does not patch the global
93
+ - `stack` includes the stacks of the aggregated errors, indented
94
+ - Takes any iterable of errors and the native `{ cause }` option
99
95
 
100
96
  Example with `cleanErrorStack`:
101
97
 
102
- ```ts
98
+ ```js
103
99
  import { cleanErrorStack, AggregateError } from "@jchip/error";
104
100
 
101
+ function loadConfig() {
102
+ return JSON.parse("{ bad json");
103
+ }
104
+
105
105
  try {
106
- require("bad");
106
+ loadConfig();
107
107
  } catch (err) {
108
- console.log(cleanErrorStack(new AggregateError([err], "require failed")));
108
+ console.log(cleanErrorStack(new AggregateError([err], "config failed")));
109
109
  }
110
110
  ```
111
111
 
112
112
  Output:
113
113
 
114
114
  ```
115
- AggregateError: require failed
116
- at Object.<anonymous> (test/samples.js:12:31)
117
- Error: Cannot find module 'bad'
118
- Require stack:
119
- - /Users/joel/error/test/samples.js
120
- at Object.<anonymous> (test/samples.js:10:3)
115
+ AggregateError: config failed
116
+ at test/app.mjs:10:31
117
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
118
+ at loadConfig (test/app.mjs:4:15)
119
+ at test/app.mjs:8:3
121
120
  ```
122
121
 
123
- vs:
122
+ vs `new AggregateError([err], "config failed").stack`:
124
123
 
125
124
  ```
126
- AggregateError: require failed
127
- at Object.<anonymous> (/Users/joel/error/test/samples.js:12:15)
128
- at Module._compile (node:internal/modules/cjs/loader:1101:14)
129
- at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
130
- at Module.load (node:internal/modules/cjs/loader:981:32)
131
- at Function.Module._load (node:internal/modules/cjs/loader:822:12)
132
- at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
133
- at node:internal/main/run_main_module:17:47
134
- Error: Cannot find module 'bad'
135
- Require stack:
136
- - /Users/joel/error/test/samples.js
137
- at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
138
- at Function.Module._load (node:internal/modules/cjs/loader:778:27)
139
- at Module.require (node:internal/modules/cjs/loader:1005:19)
140
- at require (node:internal/modules/cjs/helpers:94:18)
141
- at Object.<anonymous> (/Users/joel/error/test/samples.js:10:3)
142
- at Module._compile (node:internal/modules/cjs/loader:1101:14)
143
- at Object.Module._extensions..js (node:internal/modules/cjs/loader:1153:10)
144
- at Module.load (node:internal/modules/cjs/loader:981:32)
145
- at Function.Module._load (node:internal/modules/cjs/loader:822:12)
146
- at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:79:12)
125
+ AggregateError: config failed
126
+ at file:///home/me/app/test/app.mjs:10:15
127
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
128
+ at async node:internal/modules/esm/loader:647:26
129
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
130
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
131
+ at JSON.parse (<anonymous>)
132
+ at loadConfig (file:///home/me/app/test/app.mjs:4:15)
133
+ at file:///home/me/app/test/app.mjs:8:3
134
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
135
+ at async node:internal/modules/esm/loader:647:26
136
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
147
137
  ```
148
138
 
149
139
  [aggregateerror]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
140
+
150
141
  [license-image]: https://img.shields.io/npm/l/@jchip/error.svg
151
142
  [license-url]: LICENSE
152
143
  [build-image]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
@@ -155,4 +146,3 @@ AggregateError: require failed
155
146
  [downloads-url]: https://npm-stat.com/charts.html?package=%40jchip%2Ferror
156
147
  [npm-badge-png]: https://nodei.co/npm/@jchip/error.png?downloads=true&stars=true
157
148
  [package-url]: https://npmjs.com/package/@jchip/error
158
- [npm-version-svg]: https://versionbadg.es/jchip/error.svg
package/dist/index.d.ts CHANGED
@@ -41,6 +41,8 @@ export declare class AggregateError extends globalThis.AggregateError {
41
41
  stack: string;
42
42
  /** original error stack before generating an aggregate one */
43
43
  __stack: string;
44
- constructor(errors?: any[], msg?: string);
44
+ constructor(errors?: Iterable<any>, msg?: string, options?: {
45
+ cause?: unknown;
46
+ });
45
47
  }
46
48
  export {};
package/dist/index.js CHANGED
@@ -21,23 +21,30 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
21
21
  if (!line.match(/ {4,}at/)) {
22
22
  return line;
23
23
  }
24
- const match = line.match(/( {4,}at)([^\(]+\()([^\)]+\))(.*)/);
25
- // skip any stack tracing line not in these formats:
26
- // - " at Blah (/foo/bar:##:##)" format
27
- // - "scheme://path" (ie: webpack://path)
28
- if (!match || (!match[3].match(/[^:]+:\/\//) && !Path.isAbsolute(match[3]))) {
24
+ // " at name (location)" or " at location", the second for anonymous functions and
25
+ // ESM top-level code. The location runs to the last ")" so a ")" inside it does not cut it short.
26
+ const match = line.match(/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/);
27
+ const [, at, name = "", location = match === null || match === void 0 ? void 0 : match[5], tail = ""] = match || [];
28
+ // keep only locations that are absolute paths or "scheme://path" (ie: file://, webpack://)
29
+ if (!location || (!location.match(/[^:]+:\/\//) && !Path.isAbsolute(location))) {
29
30
  return false;
30
31
  }
31
- const path = match[3].replace(/\\/g, "/");
32
+ const path = location.replace(/\\/g, "/");
32
33
  if (defaultPathFilter
33
34
  .concat(ignorePathFilter)
34
35
  .find((s) => s && (s instanceof RegExp ? path.match(s) : path.includes(s)))) {
35
36
  return false;
36
37
  }
37
- const path2 = replacePath && replacePath.length > 1 ? path.replace(replacePath, "") : path;
38
- return `${match[1]}${match[2]}${path2}${match[4]}`;
38
+ let path2 = path;
39
+ if (replacePath && replacePath.length > 1) {
40
+ // an ESM location is a file:// URL, so drop the scheme along with replacePath
41
+ path2 = path.startsWith(`file://${replacePath}`)
42
+ ? path.slice(`file://${replacePath}`.length)
43
+ : path.replace(replacePath, "");
44
+ }
45
+ return `${at}${name}${path2}${tail}`;
39
46
  })
40
- .filter((x) => x)
47
+ .filter((x) => x !== false)
41
48
  .join("\n");
42
49
  return result;
43
50
  }
@@ -65,18 +72,22 @@ export function aggregateStack(stack, errors) {
65
72
  * @returns aggregate stack
66
73
  */
67
74
  export function aggregateErrorStack(error) {
68
- return aggregateStack(error.__stack || error.message || String(error), error.errors);
75
+ // our AggregateError's stack getter calls this, so only read stack from other errors
76
+ const stack = error.__stack || (error instanceof AggregateError ? "" : error.stack);
77
+ return aggregateStack(stack || error.message || String(error), error.errors);
69
78
  }
70
79
  /**
71
80
  * AggregateError
72
81
  * - https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
73
82
  */
74
83
  export class AggregateError extends globalThis.AggregateError {
75
- constructor(errors, msg) {
84
+ constructor(errors, msg, options) {
76
85
  if (!errors || !(errors[Symbol.iterator] instanceof Function)) {
77
86
  throw new TypeError(`input errors must be iterable but it's ${typeof errors}`);
78
87
  }
79
- super(errors, msg);
88
+ // spread once: super() would consume a one-shot iterator such as a generator
89
+ const list = Array.from(errors);
90
+ super(list, msg, options);
80
91
  // Using defineProperty to replicate behavior of Object.keys(new Error()) returns []
81
92
  Object.defineProperty(this, "name", { value: "AggregateError" });
82
93
  let aggStack;
@@ -86,7 +97,7 @@ export class AggregateError extends globalThis.AggregateError {
86
97
  configurable: true,
87
98
  enumerable: false,
88
99
  writable: true,
89
- value: [].concat(errors),
100
+ value: list,
90
101
  },
91
102
  // save original stack
92
103
  __stack: {
@@ -0,0 +1,159 @@
1
+ # @jchip/error reference
2
+
3
+ `@jchip/error` has helpers to print readable error stacks and an `AggregateError` subclass whose `stack` includes the stacks of the errors it holds. It has no runtime dependencies. ESM only (`"type": "module"`). Requires Node `^22.22.2 || ^24.15.0 || >=26.0.0`.
4
+
5
+ ## Imports
6
+
7
+ ```js
8
+ import { cleanErrorStack, aggregateStack, aggregateErrorStack, AggregateError } from "@jchip/error";
9
+ import * as jerr from "@jchip/error";
10
+ ```
11
+
12
+ Everything is a named export from the package root. There is no default export and there are no subpaths (only `@jchip/error/package.json`).
13
+
14
+ Runtime exports: `cleanErrorStack`, `aggregateStack`, `aggregateErrorStack`, `AggregateError`.
15
+
16
+ There are no exported types besides the class `AggregateError`. The options type `CleanErrorStackOptions` is declared but not exported, so it cannot be imported by name. Use `Parameters<typeof cleanErrorStack>[1]` to get it.
17
+
18
+ ## `cleanErrorStack(error, options?)`
19
+
20
+ ```ts
21
+ type CleanErrorStackOptions = {
22
+ replacePath?: false | string;
23
+ ignorePathFilter?: (string | RegExp)[];
24
+ };
25
+
26
+ function cleanErrorStack(
27
+ error: Error,
28
+ options?: CleanErrorStackOptions
29
+ ): string
30
+ ```
31
+
32
+ Returns the text of `error.stack` with noisy stack frames removed and file paths shortened. The error is not modified.
33
+
34
+ | option | type | default | behavior |
35
+ | --- | --- | --- | --- |
36
+ | `replacePath` | `false \| string` | `` `${process.cwd()}/` `` (evaluated on each call) | Substring removed from each kept frame path. Ignored when `false`, empty, or exactly 1 character long (so `"/"` does nothing). |
37
+ | `ignorePathFilter` | `(string \| RegExp)[]` | `[]` | Extra filters. A frame whose path matches any filter is dropped. Added to the built-in filter. |
38
+
39
+ Input selection:
40
+
41
+ - The text cleaned is `error.stack`, or `error.message` when the stack is falsy.
42
+ - When neither exists (or `error` is `null`/`undefined`), nothing is cleaned and the return value is `String(error && (error.stack || error.message))`. That gives `"null"` for `null` and `"undefined"` for `undefined`, `{}`, or an object with empty stack and no message.
43
+ - A non-string, truthy `stack` (such as a number) throws `TypeError` (`stack.split is not a function`).
44
+
45
+ Processing, applied to each line of the text split on `"\n"`:
46
+
47
+ 1. A line that does not match `/ {4,}at/` (an `at` preceded by 4 or more spaces anywhere in the line) is kept unchanged. This covers the message line, `Require stack:` lines, and frames indented with fewer than 4 spaces.
48
+ 2. A frame line is parsed with `/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/`. It has two forms. `at name (location)` takes the location from the first `(` to the last `)`, so a `)` inside a path does not cut it short. `at location` has no parentheses. Node uses it for anonymous functions and ES module top-level code.
49
+ 3. The frame is dropped when parsing fails (such as a bare ` at`), or unless the location contains `scheme://` (regex `/[^:]+:\/\//`) or `path.isAbsolute(location)` is true. That removes `node:internal/...` frames in either form, `(<anonymous>)`, `(index 0)`, and relative paths. `path.isAbsolute` is platform specific, so a Windows path like `C:\a\b.js` is dropped on POSIX.
50
+ 4. Backslashes in the location are replaced with `/`.
51
+ 5. The frame is dropped if the location matches the built-in filter or any `ignorePathFilter` entry. A `RegExp` entry uses `location.match(re)`. A string entry uses `location.includes(str)`. Falsy entries (such as `""`) are skipped.
52
+ 6. Otherwise `replacePath` (when usable) is removed. A location that starts with `file://` + `replacePath` loses both, so an ES module URL under the current directory becomes a relative path. Any other location has the first occurrence of `replacePath` removed with `String.prototype.replace` (plain string, not a regex). It can match anywhere in the location, not only at the start. A `file://` URL outside `replacePath` therefore stays whole.
53
+ 7. The kept frame is rebuilt as `at` + name + new location + the text after the last `)`. Text before the 4-space indent is not kept.
54
+ 8. Dropped frames are removed and the remaining lines are joined with `"\n"`. Blank lines in the original text are kept.
55
+
56
+ Real stacks from CommonJS and ES modules are covered by tests on Node 22, 24 and 26 (`test/spec/real-stack.spec.ts`).
57
+
58
+ Built-in filter (always on, checked before `ignorePathFilter`):
59
+
60
+ ```
61
+ /\/node_modules.*\/(pirates\/|isomorphic-loader\/lib\/extend-require)/
62
+ ```
63
+
64
+ It drops frames from the `pirates` and `isomorphic-loader` require hooks. It matches on the backslash-normalized path.
65
+
66
+ Not removed: frames from other `node_modules` packages, and any absolute path outside `replacePath`. Only `node:` internals and other non-absolute locations go by rule 3.
67
+
68
+ ```js
69
+ cleanErrorStack(err);
70
+ // Error: boom
71
+ // at run (src/app.js:10:5)
72
+ // at src/main.mjs:3:1
73
+ cleanErrorStack(err, { replacePath: false, ignorePathFilter: ["/node_modules/", /vitest/] });
74
+ ```
75
+
76
+ ## `aggregateStack(stack, errors)`
77
+
78
+ ```ts
79
+ function aggregateStack(stack: string, errors: any[]): string
80
+ ```
81
+
82
+ Builds a text from a top stack and a list of errors. Output is `stack`, then one entry per error, all joined with `"\n"`. Each error entry is its text with every line indented by 2 spaces (`replace(/^/gm, " ")`, so blank lines become `" "`).
83
+
84
+ The text of one error `e` is `e.stack`, else `e.message`, else `String(e)`. A falsy `e` gives `String(e)`. Examples: `1` gives `"1"`, `null` gives `"null"`, `{ message: "mm" }` gives `"mm"`.
85
+
86
+ Nested aggregate stacks indent again, so each nesting level adds 2 spaces.
87
+
88
+ Edge cases on `errors`:
89
+
90
+ - An array (or any object with a `map` function) is mapped. `[]` returns `stack` alone with no trailing newline.
91
+ - A falsy `errors` (`null`, `undefined`, `""`) or a value with no `map` function (such as a string or a plain object without `map`) is treated as no errors, but the result gets a trailing `"\n"`, for example `aggregateStack("t", null) === "t\n"`.
92
+ - A truthy `errors.map` that is not a function throws `TypeError`.
93
+ - `stack` is not checked. A non-string is joined as is.
94
+
95
+ ## `aggregateErrorStack(error)`
96
+
97
+ ```ts
98
+ function aggregateErrorStack(error: AggregateError): string
99
+ ```
100
+
101
+ Returns `aggregateStack(top, error.errors)`. The parameter type is this package's `AggregateError` class.
102
+
103
+ - For an instance of this package's `AggregateError`, `top` is its saved original stack (`__stack`), which is the native stack text, including the header `AggregateError: <message>` and frames.
104
+ - For any other object, such as a native `globalThis.AggregateError`, `top` is `error.stack`. The native header and frames are kept.
105
+ - When that text is empty, `top` falls back to `error.message`, then `String(error)`. An instance of this class with an empty `__stack` never reads its own `stack`, since that getter calls this function.
106
+ - The list used is `error.errors`.
107
+ - The result is computed each time. It is cached only by the `stack` getter of this package's class.
108
+
109
+ ```js
110
+ console.log(aggregateErrorStack(new AggregateError([new Error("error 1")], "test")));
111
+ // AggregateError: test
112
+ // at ...
113
+ // Error: error 1
114
+ // at ...
115
+ ```
116
+
117
+ ## `AggregateError`
118
+
119
+ ```ts
120
+ class AggregateError extends globalThis.AggregateError {
121
+ readonly name: string; // "AggregateError"
122
+ errors: any[];
123
+ stack: string;
124
+ __stack: string;
125
+ constructor(errors?: Iterable<any>, msg?: string, options?: { cause?: unknown });
126
+ }
127
+ ```
128
+
129
+ An `AggregateError` whose `stack` also prints the errors it wraps. It is a subclass, not a polyfill. See "Polyfill behavior" below.
130
+
131
+ Constructor `new AggregateError(errors, msg?, options?)`:
132
+
133
+ - `errors` must be truthy and have a callable `[Symbol.iterator]`. Otherwise it throws `TypeError` with the message `input errors must be iterable but it's ${typeof errors}`. Falsy values give `undefined`, `object` (for `null`), `number`, and so on. A truthy number (`5`) gives `number`. A string is accepted since strings are iterable.
134
+ - `errors` is spread once with `Array.from`, and that list goes to both the parent constructor and `errors`. So a `Set` or a one-shot iterator such as a generator gives its elements, and a string `"ab"` gives `["a", "b"]`, as with the native class.
135
+ - `msg` is the message. When omitted the message is `""`.
136
+ - `options` is passed to the parent constructor, so `{ cause }` sets `cause` as the native class does.
137
+
138
+ Instance properties (all defined with `Object.defineProperty`, so `Object.keys(err)` is `[]`):
139
+
140
+ | property | attributes | value |
141
+ | --- | --- | --- |
142
+ | `name` | non-enumerable, non-writable, non-configurable own property | `"AggregateError"`. A subclass cannot override it by assignment. |
143
+ | `errors` | non-enumerable, writable, configurable | `Array.from(errors)`: a new array, even when `errors` is an array. |
144
+ | `__stack` | non-enumerable, non-writable, non-configurable | `this.stack` read right after `super()`: the native stack text of the error. |
145
+ | `stack` | non-enumerable, configurable accessor | Getter returning `aggregateErrorStack(this)`, computed on first read and cached. |
146
+
147
+ `stack` details:
148
+
149
+ - The cache means `errors` changes made after the first read of `stack` are not reflected. Changes made before the first read are.
150
+ - Only a getter is defined. The existing native setter of the engine remains, so assigning to `err.stack` does not throw and has no effect on the getter result (observed on Node 24).
151
+ - `instanceof` works against the native class (`new AggregateError([]) instanceof globalThis.AggregateError` is `true`). A native instance is not an instance of this class.
152
+ - `console.log(err)` and `err.stack` in a debugger show the aggregate form. `cleanErrorStack(err)` therefore also cleans the nested error stacks, and the nested lines keep their 2-space indent.
153
+
154
+ ## Polyfill behavior
155
+
156
+ - The package does not patch or define any global. Importing it has no side effects (`"sideEffects": false`).
157
+ - `AggregateError` here is a different class from `globalThis.AggregateError` (`AggregateError !== globalThis.AggregateError`). Its prototype parent is the global one.
158
+ - The module evaluates `class AggregateError extends globalThis.AggregateError` at import time. If the runtime lacks a global `AggregateError`, import throws. All supported Node versions (`engines`) have it, so there is no fallback implementation.
159
+ - Code that wants the aggregate-stack behavior must import this class explicitly. Native `AggregateError` instances are unchanged, and `aggregateErrorStack(native)` is the way to format them.
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@jchip/error",
3
- "version": "2.1.6",
3
+ "version": "2.1.8",
4
4
  "description": "utilities and polyfill for node.js errors",
5
- "homepage": "https://github.com/jchip/fynjs/tree/main/packages/error",
5
+ "homepage": "https://fynjs.pages.dev/api/modules/_jchip_error",
6
6
  "type": "module",
7
7
  "types": "./dist/index.d.ts",
8
8
  "main": "./dist/index.js",
@@ -43,17 +43,17 @@
43
43
  "@types/node": "^26.4.1",
44
44
  "@vitest/coverage-v8": "^5.0.0",
45
45
  "prettier": "^3.5.3",
46
- "publish-util": "^3.1.6",
46
+ "publish-util": "^3.1.8",
47
47
  "typescript": "^7.0.2",
48
48
  "vite": "^8.2.2",
49
49
  "vitest": "^5.0.0"
50
50
  },
51
51
  "files": [
52
- "dist"
52
+ "dist",
53
+ "docs/reference.md"
53
54
  ],
54
55
  "publishConfig": {
55
- "access": "public",
56
- "registry": "https://registry.npmjs.com/"
56
+ "access": "public"
57
57
  },
58
58
  "bugs": {
59
59
  "url": "https://github.com/jchip/fynjs/issues"