@jchip/error 2.1.7 → 3.0.0

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,69 @@ 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
+ - Assigning `err.stack` replaces the top part and keeps the aggregated stacks
95
+ - Takes any iterable of errors and the native `{ cause }` option
99
96
 
100
97
  Example with `cleanErrorStack`:
101
98
 
102
- ```ts
99
+ ```js
103
100
  import { cleanErrorStack, AggregateError } from "@jchip/error";
104
101
 
102
+ function loadConfig() {
103
+ return JSON.parse("{ bad json");
104
+ }
105
+
105
106
  try {
106
- require("bad");
107
+ loadConfig();
107
108
  } catch (err) {
108
- console.log(cleanErrorStack(new AggregateError([err], "require failed")));
109
+ console.log(cleanErrorStack(new AggregateError([err], "config failed")));
109
110
  }
110
111
  ```
111
112
 
112
113
  Output:
113
114
 
114
115
  ```
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)
116
+ AggregateError: config failed
117
+ at test/app.mjs:10:31
118
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
119
+ at loadConfig (test/app.mjs:4:15)
120
+ at test/app.mjs:8:3
121
121
  ```
122
122
 
123
- vs:
123
+ vs `new AggregateError([err], "config failed").stack`:
124
124
 
125
125
  ```
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)
126
+ AggregateError: config failed
127
+ at file:///home/me/app/test/app.mjs:10:15
128
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
129
+ at async node:internal/modules/esm/loader:647:26
130
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
131
+ SyntaxError: Expected property name or '}' in JSON at position 2 (line 1 column 3)
132
+ at JSON.parse (<anonymous>)
133
+ at loadConfig (file:///home/me/app/test/app.mjs:4:15)
134
+ at file:///home/me/app/test/app.mjs:8:3
135
+ at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
136
+ at async node:internal/modules/esm/loader:647:26
137
+ at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:101:5)
147
138
  ```
148
139
 
149
140
  [aggregateerror]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError
141
+
150
142
  [license-image]: https://img.shields.io/npm/l/@jchip/error.svg
151
143
  [license-url]: LICENSE
152
144
  [build-image]: https://github.com/jchip/fynjs/actions/workflows/ci.yml/badge.svg
@@ -155,4 +147,3 @@ AggregateError: require failed
155
147
  [downloads-url]: https://npm-stat.com/charts.html?package=%40jchip%2Ferror
156
148
  [npm-badge-png]: https://nodei.co/npm/@jchip/error.png?downloads=true&stars=true
157
149
  [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
@@ -14,30 +14,39 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
14
14
  if (!stack) {
15
15
  return String(stack);
16
16
  }
17
+ // normalize CRLF so every output line ends the same way
17
18
  const result = stack
18
- .split("\n")
19
+ .split(/\r?\n/)
19
20
  .map((line) => {
20
21
  // keep all non stack tracing lines
21
22
  if (!line.match(/ {4,}at/)) {
22
23
  return line;
23
24
  }
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]))) {
25
+ // " at name (location)" or " at location", the second for anonymous functions and
26
+ // ESM top-level code. The location runs to the last ")" so a ")" inside it does not cut it short.
27
+ const match = line.match(/( {4,}at )(?:([^(]*\()(.+)(\).*)|(.+))/);
28
+ const [, at, name = "", location = match === null || match === void 0 ? void 0 : match[5], tail = ""] = match || [];
29
+ // keep only locations that are absolute paths or "scheme://path" (ie: file://, webpack://)
30
+ if (!location || (!location.match(/[^:]+:\/\//) && !Path.isAbsolute(location))) {
29
31
  return false;
30
32
  }
31
- const path = match[3].replace(/\\/g, "/");
33
+ const path = location.replace(/\\/g, "/");
32
34
  if (defaultPathFilter
33
35
  .concat(ignorePathFilter)
34
36
  .find((s) => s && (s instanceof RegExp ? path.match(s) : path.includes(s)))) {
35
37
  return false;
36
38
  }
37
- const path2 = replacePath && replacePath.length > 1 ? path.replace(replacePath, "") : path;
38
- return `${match[1]}${match[2]}${path2}${match[4]}`;
39
+ let path2 = path;
40
+ if (replacePath && replacePath.length > 1) {
41
+ // strip replacePath only as a leading prefix. An ESM location is a file:// URL, so drop
42
+ // the scheme along with it.
43
+ const prefix = path.startsWith(`file://${replacePath}`) ? `file://${replacePath}` : replacePath;
44
+ if (path.startsWith(prefix))
45
+ path2 = path.slice(prefix.length);
46
+ }
47
+ return `${at}${name}${path2}${tail}`;
39
48
  })
40
- .filter((x) => x)
49
+ .filter((x) => x !== false)
41
50
  .join("\n");
42
51
  return result;
43
52
  }
@@ -49,14 +58,13 @@ export function cleanErrorStack(error, { replacePath = `${process.cwd()}/`, igno
49
58
  * @returns aggregate stack
50
59
  */
51
60
  export function aggregateStack(stack, errors) {
52
- return [stack]
53
- .concat(errors &&
54
- errors.map &&
55
- errors.map((e) => {
61
+ const stacks = errors && errors.map
62
+ ? errors.map((e) => {
56
63
  const s = e && (e.stack || e.message);
57
64
  return (s || String(e)).replace(/^/gm, " ");
58
- }))
59
- .join("\n");
65
+ })
66
+ : [];
67
+ return [stack].concat(stacks).join("\n");
60
68
  }
61
69
  /**
62
70
  * build the aggregate stack of an AggregateError
@@ -65,18 +73,22 @@ export function aggregateStack(stack, errors) {
65
73
  * @returns aggregate stack
66
74
  */
67
75
  export function aggregateErrorStack(error) {
68
- return aggregateStack(error.__stack || error.message || String(error), error.errors);
76
+ // our AggregateError's stack getter calls this, so only read stack from other errors
77
+ const stack = error.__stack || (error instanceof AggregateError ? "" : error.stack);
78
+ return aggregateStack(stack || error.message || String(error), error.errors);
69
79
  }
70
80
  /**
71
81
  * AggregateError
72
82
  * - https://tc39.es/ecma262/multipage/fundamental-objects.html#sec-aggregate-error-objects
73
83
  */
74
84
  export class AggregateError extends globalThis.AggregateError {
75
- constructor(errors, msg) {
85
+ constructor(errors, msg, options) {
76
86
  if (!errors || !(errors[Symbol.iterator] instanceof Function)) {
77
87
  throw new TypeError(`input errors must be iterable but it's ${typeof errors}`);
78
88
  }
79
- super(errors, msg);
89
+ // spread once: super() would consume a one-shot iterator such as a generator
90
+ const list = Array.from(errors);
91
+ super(list, msg, options);
80
92
  // Using defineProperty to replicate behavior of Object.keys(new Error()) returns []
81
93
  Object.defineProperty(this, "name", { value: "AggregateError" });
82
94
  let aggStack;
@@ -86,18 +98,23 @@ export class AggregateError extends globalThis.AggregateError {
86
98
  configurable: true,
87
99
  enumerable: false,
88
100
  writable: true,
89
- value: [].concat(errors),
101
+ value: list,
90
102
  },
91
103
  // save original stack
92
104
  __stack: {
93
105
  enumerable: false,
106
+ writable: true,
94
107
  value: this.stack,
95
108
  },
96
- // make aggregate stack
109
+ // make aggregate stack. Writing it replaces the header and keeps the errors' stacks.
97
110
  stack: {
98
111
  get() {
99
112
  return aggStack || (aggStack = aggregateErrorStack(this));
100
113
  },
114
+ set(value) {
115
+ this.__stack = value;
116
+ aggStack = "";
117
+ },
101
118
  },
102
119
  });
103
120
  }
@@ -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) | Leading prefix 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"` or `"\r\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 loses `replacePath` only when it starts with it (plain string, not a regex). A match in the middle of a path is left alone, and so is a `file://` URL outside `replacePath`.
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. A CRLF stack therefore comes back with LF line endings throughout.
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.
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, and also returns `stack` alone, for example `aggregateStack("t", null) === "t"`.
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, writable, non-configurable | `this.stack` read right after `super()`: the native stack text of the error. Writing `stack` replaces it. |
145
+ | `stack` | non-enumerable, configurable accessor | Getter returning `aggregateErrorStack(this)`, computed on first read and cached. The setter replaces the header. |
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
+ - Assigning `err.stack = text` replaces the header: `__stack` becomes `text` and the cache is cleared. The next read gives `text` followed by the errors' indented stacks, so the aggregated stacks are kept.
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.7",
3
+ "version": "3.0.0",
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,13 +43,14 @@
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.7",
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
56
  "access": "public"