@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 +64 -73
- package/dist/index.d.ts +3 -1
- package/dist/index.js +38 -21
- package/docs/reference.md +159 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @jchip/error
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
```
|
|
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
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
at
|
|
51
|
-
at
|
|
52
|
-
at
|
|
53
|
-
at
|
|
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
|
-
```
|
|
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
|
|
79
|
-
at
|
|
80
|
-
at
|
|
81
|
-
at
|
|
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
|
|
87
|
-
at
|
|
88
|
-
at
|
|
89
|
-
at
|
|
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
|
-
-
|
|
98
|
-
-
|
|
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
|
-
```
|
|
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
|
-
|
|
107
|
+
loadConfig();
|
|
107
108
|
} catch (err) {
|
|
108
|
-
console.log(cleanErrorStack(new AggregateError([err], "
|
|
109
|
+
console.log(cleanErrorStack(new AggregateError([err], "config failed")));
|
|
109
110
|
}
|
|
110
111
|
```
|
|
111
112
|
|
|
112
113
|
Output:
|
|
113
114
|
|
|
114
115
|
```
|
|
115
|
-
AggregateError:
|
|
116
|
-
at
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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:
|
|
127
|
-
at
|
|
128
|
-
at
|
|
129
|
-
at
|
|
130
|
-
at
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
at
|
|
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
|
|
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(
|
|
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
|
-
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 =
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
53
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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:
|
|
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": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "utilities and polyfill for node.js errors",
|
|
5
|
-
"homepage": "https://
|
|
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.
|
|
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"
|