unknown-error 0.1.0 → 0.2.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 +57 -43
- package/WHY-UNKNOWN-ERROR.md +19 -19
- package/dist/lib/safe.js +1 -4
- package/dist/lib/walk.d.ts +2 -2
- package/package.json +10 -11
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<div style="text-align:center">
|
|
2
2
|
|
|
3
3
|
# unknown-error
|
|
4
|
+
|
|
4
5
|
Turn any caught `unknown` into a real Error without throwing, even for hostile values, other realms and deep or circular cause chains.
|
|
5
6
|
|
|
6
7
|
[](https://www.npmjs.com/package/unknown-error)
|
|
@@ -14,9 +15,10 @@ Turn any caught `unknown` into a real Error without throwing, even for hostile v
|
|
|
14
15
|
- **Whole error trees**: Walks `cause` chains and `AggregateError` entries without recursion, so circular or 100,000-deep chains end instead of overflowing the stack.
|
|
15
16
|
- **Doesn't modify your errors**: Errors are returned as they are, never patched in place.
|
|
16
17
|
|
|
17
|
-
For why it exists and the design decisions behind it, see [WHY-UNKNOWN-ERROR.md](https://github.com/JacobLey/leyman/blob/main/tools/unknown-error/WHY-UNKNOWN-ERROR.md).
|
|
18
|
+
Guides and the full reference are on the [docs site](https://jacobley.github.io/leyman/packages/unknown-error/). For why it exists and the design decisions behind it, see [WHY-UNKNOWN-ERROR.md](https://github.com/JacobLey/leyman/blob/main/tools/unknown-error/WHY-UNKNOWN-ERROR.md).
|
|
18
19
|
|
|
19
20
|
## Contents
|
|
21
|
+
|
|
20
22
|
- [Install](#install)
|
|
21
23
|
- [Example](#example)
|
|
22
24
|
- [Usage](#usage)
|
|
@@ -56,9 +58,9 @@ export const loadConfig = async (): Promise<Config> => {
|
|
|
56
58
|
|
|
57
59
|
## Usage
|
|
58
60
|
|
|
59
|
-
ESM only. Runs in Node.js
|
|
61
|
+
ESM only. Runs in Node.js 22 or later, Deno, Bun and browsers, with no Node.js APIs. From CommonJS, load it with a dynamic import: `const { toError } = await import('unknown-error');`
|
|
60
62
|
|
|
61
|
-
Errors are recognized with [`Error.isError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError) where the runtime has it: Node.js 24, Chrome 134, Firefox 138, Safari 18.4 and later. In older browsers, they are recognized by `instanceof Error` or their `[object Error]` tag instead, which works across realms too, but also lets through a Proxy around an Error and an object with a forged `Symbol.toStringTag`.
|
|
63
|
+
Errors are recognized with [`Error.isError`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/isError) where the runtime has it: Node.js 24, Chrome 134, Firefox 138, Safari 18.4 and later. In Node.js 22 and older browsers, they are recognized by `instanceof Error` or their `[object Error]` tag instead, which works across realms too, but also lets through a Proxy around an Error and an object with a forged `Symbol.toStringTag`.
|
|
62
64
|
|
|
63
65
|
In Safari and in Node.js before 24.3, `Error.isError` returns `false` for a `DOMException`, so there a `DOMException` is rebuilt as a plain Error with the same name and message rather than returned as-is.
|
|
64
66
|
|
|
@@ -70,11 +72,11 @@ Every function takes `unknown`, so pass the `catch` variable straight in. None o
|
|
|
70
72
|
|
|
71
73
|
Converts any caught value to an Error.
|
|
72
74
|
|
|
73
|
-
| `value`
|
|
74
|
-
|
|
75
|
-
| An Error, including one from another realm | The same Error, unmodified
|
|
76
|
-
| An object with a string `message`
|
|
77
|
-
| Anything else
|
|
75
|
+
| `value` | Result |
|
|
76
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
77
|
+
| An Error, including one from another realm | The same Error, unmodified |
|
|
78
|
+
| An object with a string `message` | A new `Error` with the object's `message`, `name`, `stack` and `cause`, and its other own properties (like `code`) copied over |
|
|
79
|
+
| Anything else | A new [`NonError`](#nonerror) describing it |
|
|
78
80
|
|
|
79
81
|
**Returns** `Error`.
|
|
80
82
|
|
|
@@ -86,7 +88,19 @@ toError('boom'); // NonError: boom
|
|
|
86
88
|
toError({ count: 1n }); // NonError: {"count":"1n"}
|
|
87
89
|
```
|
|
88
90
|
|
|
89
|
-
An error-like object without a `stack` gets a stack pointing at the `toError` call, since the original location is unknown.
|
|
91
|
+
An error-like object without a `stack` gets a stack pointing at the `toError` call, since the original location is unknown. Properties the new Error already has, own or inherited, are not copied, so a JSON-parsed `"__proto__"` key can't replace its prototype.
|
|
92
|
+
|
|
93
|
+
A [`NonError`](#nonerror) message describes the value as follows:
|
|
94
|
+
|
|
95
|
+
| Value | Message |
|
|
96
|
+
| -------------------------------------------- | ----------------------------------------------------------------------- |
|
|
97
|
+
| String | The string itself |
|
|
98
|
+
| `null`, `undefined`, number, boolean, symbol | `String(value)`, like `'null'` or `'Symbol(id)'` |
|
|
99
|
+
| BigInt | `'10n'` |
|
|
100
|
+
| Function | `'[Function name]'`, never its source |
|
|
101
|
+
| Object or array | JSON, with circular references as `"[Circular]"` and BigInts as `"10n"` |
|
|
102
|
+
|
|
103
|
+
When an object's JSON is empty (`toJSON` returns `undefined`) or throws, the message falls back to `String(value)`, then to the object's tag (`[object Object]`), and finally to `[unprintable value]`.
|
|
90
104
|
|
|
91
105
|
### `getErrorMessage(value)`
|
|
92
106
|
|
|
@@ -103,10 +117,10 @@ getErrorMessage(404); // '404'
|
|
|
103
117
|
|
|
104
118
|
Whether `value` is an Error whose `code` property equals `code`. A type guard, so it narrows `value` to `Error & { readonly code: typeof code }`.
|
|
105
119
|
|
|
106
|
-
| Parameter | Type
|
|
107
|
-
|
|
108
|
-
| `value`
|
|
109
|
-
| `code`
|
|
120
|
+
| Parameter | Type | Default | Description |
|
|
121
|
+
| --------- | ------------------ | ------- | ----------------------------------- |
|
|
122
|
+
| `value` | `unknown` | — | A caught value. |
|
|
123
|
+
| `code` | `string \| number` | — | The code to match, like `'ENOENT'`. |
|
|
110
124
|
|
|
111
125
|
**Returns** `boolean`.
|
|
112
126
|
|
|
@@ -118,40 +132,40 @@ Only real Errors match. To match error-like objects too, convert them first: `ha
|
|
|
118
132
|
|
|
119
133
|
#### `new NonError(thrown, options?)`
|
|
120
134
|
|
|
121
|
-
| Parameter | Type
|
|
122
|
-
|
|
123
|
-
| `thrown`
|
|
124
|
-
| `options` | `ErrorOptions` | —
|
|
135
|
+
| Parameter | Type | Default | Description |
|
|
136
|
+
| --------- | -------------- | ------- | ---------------------------------------- |
|
|
137
|
+
| `thrown` | `unknown` | — | The value that was thrown. |
|
|
138
|
+
| `options` | `ErrorOptions` | — | Standard Error options, such as `cause`. |
|
|
125
139
|
|
|
126
140
|
#### Properties
|
|
127
141
|
|
|
128
|
-
| Property
|
|
129
|
-
|
|
130
|
-
| `name`
|
|
131
|
-
| `message` | `string`
|
|
132
|
-
| `thrown`
|
|
142
|
+
| Property | Type | Description |
|
|
143
|
+
| --------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
144
|
+
| `name` | `'NonError'` | |
|
|
145
|
+
| `message` | `string` | A description of `thrown`: strings as-is, objects as JSON (with circular references and BigInts handled), functions as `[Function name]`. Cut to 1,000 characters. |
|
|
146
|
+
| `thrown` | `unknown` | The original value. |
|
|
133
147
|
|
|
134
148
|
### `walkErrors(value, options?)`
|
|
135
149
|
|
|
136
150
|
Visits `value`, its `cause` chain and the `errors` of any `AggregateError`, depth first: an error, then its cause, then its `errors` in order.
|
|
137
151
|
|
|
138
|
-
Every value is converted with `toError`, so a cause that isn't an Error still appears. A value already visited is skipped, so circular chains end.
|
|
152
|
+
Every value is converted with `toError`, so a cause that isn't an Error still appears. A value already visited is skipped, so circular chains end. Primitives have no identity, so the same string can be visited more than once.
|
|
139
153
|
|
|
140
|
-
| Parameter
|
|
141
|
-
|
|
142
|
-
| `value`
|
|
143
|
-
| `options.maxDepth` | `number`
|
|
154
|
+
| Parameter | Type | Default | Description |
|
|
155
|
+
| ------------------ | --------- | ------- | ---------------------------------------------------------------------------- |
|
|
156
|
+
| `value` | `unknown` | — | A caught value. |
|
|
157
|
+
| `options.maxDepth` | `number` | `32` | Deepest level to visit. The root is level `0`. Pass `Infinity` for no limit. |
|
|
144
158
|
|
|
145
|
-
**Returns** `Generator<ErrorNode>`.
|
|
159
|
+
**Returns** `Generator<ErrorNode, void, undefined>`. The option and node types are exported as `WalkOptions`, `ErrorNode` and `ErrorRelation`.
|
|
146
160
|
|
|
147
161
|
#### `ErrorNode`
|
|
148
162
|
|
|
149
|
-
| Property
|
|
150
|
-
|
|
151
|
-
| `error`
|
|
152
|
-
| `depth`
|
|
153
|
-
| `relation`
|
|
154
|
-
| `truncated` | `boolean`
|
|
163
|
+
| Property | Type | Description |
|
|
164
|
+
| ----------- | ---------------------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `error` | `Error` | The value, converted with `toError`. |
|
|
166
|
+
| `depth` | `number` | Distance from the root, which is `0`. |
|
|
167
|
+
| `relation` | `'root' \| 'cause' \| 'aggregate'` | How it relates to its parent: the value passed in, its parent's `cause`, or an entry in its parent's `errors`. |
|
|
168
|
+
| `truncated` | `boolean` | `true` when `maxDepth` stopped the walk here, so this error's own cause and errors were skipped. |
|
|
155
169
|
|
|
156
170
|
```ts
|
|
157
171
|
const error = new AggregateError([new Error('a'), 'b'], 'all', { cause: new Error('why') });
|
|
@@ -179,17 +193,17 @@ console.log(formatError(new Error('request failed', { cause: new Error('ECONNREF
|
|
|
179
193
|
// at ...
|
|
180
194
|
```
|
|
181
195
|
|
|
182
|
-
Causes are
|
|
196
|
+
Causes are labeled `Caused by:`, and `AggregateError` entries `-`. Each error is printed as its `stack`, with a `Name: message` line added first when the stack doesn't start with one (as in Firefox and Safari). Where `maxDepth` cut the walk short, a `… more causes omitted` line says so.
|
|
183
197
|
|
|
184
198
|
## Compared to alternatives
|
|
185
199
|
|
|
186
|
-
|
|
|
187
|
-
|
|
188
|
-
| Never throws on hostile values
|
|
189
|
-
| Errors from other realms
|
|
190
|
-
| Readable circular and BigInt values | ✅
|
|
191
|
-
| Leaves your errors unmodified
|
|
192
|
-
| Causes and `AggregateError`
|
|
193
|
-
| 10,000-deep cause chains
|
|
200
|
+
| | unknown-error | [normalize-exception](https://www.npmjs.com/package/normalize-exception) | [ensure-error](https://www.npmjs.com/package/ensure-error) | [serialize-error](https://www.npmjs.com/package/serialize-error) | [pony-cause](https://www.npmjs.com/package/pony-cause) |
|
|
201
|
+
| ----------------------------------- | ------------- | ------------------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------ |
|
|
202
|
+
| Never throws on hostile values | ✅ | ✅ | ❌ | ❌ | — |
|
|
203
|
+
| Errors from other realms | ✅ | ✅ | ❌ | ✅ | ❌ |
|
|
204
|
+
| Readable circular and BigInt values | ✅ | ❌ | ❌ | ❌ | — |
|
|
205
|
+
| Leaves your errors unmodified | ✅ | ❌ | ❌ | ✅ | — |
|
|
206
|
+
| Causes and `AggregateError` | ✅ | ✅ | ✅ | — | Causes only |
|
|
207
|
+
| 10,000-deep cause chains | ✅ | ❌ | ❌ | — | ❌ |
|
|
194
208
|
|
|
195
209
|
Each cell is checked by a [comparison test suite](https://github.com/JacobLey/leyman/blob/main/e2e/unknown-error-comparison/test/integration/comparison.spec.ts). For every case, and where hand-written conversions fall short, see [WHY-UNKNOWN-ERROR.md](https://github.com/JacobLey/leyman/blob/main/tools/unknown-error/WHY-UNKNOWN-ERROR.md#comparison).
|
package/WHY-UNKNOWN-ERROR.md
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
## The Problem
|
|
8
8
|
|
|
9
|
-
In TypeScript's `strict` mode, a `catch` variable is `unknown`,
|
|
9
|
+
In TypeScript's `strict` mode, a `catch` variable is `unknown`, because JavaScript can throw anything. To log it, report it or wrap it, you need an Error, so nearly every codebase ends up with something like this:
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
12
|
const toError = (value: unknown): Error =>
|
|
13
13
|
value instanceof Error ? value : new Error(String(value));
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
It handles the cases that come to mind first: an Error, or a thrown string. It
|
|
16
|
+
It handles the cases that come to mind first: an Error, or a thrown string. It fails on anything else, and throws inside the `catch` block that was meant to handle the error.
|
|
17
17
|
|
|
18
18
|
### Errors from another realm aren't `instanceof Error`
|
|
19
19
|
|
|
@@ -57,23 +57,23 @@ Every claim below is checked by the [comparison suite](https://github.com/JacobL
|
|
|
57
57
|
|
|
58
58
|
From the [comparison suite](https://github.com/JacobLey/leyman/blob/main/e2e/unknown-error-comparison/test/integration/comparison.spec.ts). ✅ handles it, ❌ doesn't, 💥 throws, — has no API for it.
|
|
59
59
|
|
|
60
|
-
| Case
|
|
61
|
-
|
|
62
|
-
| Uses a thrown string as the message, unchanged
|
|
63
|
-
| Converts a thrown `null`
|
|
64
|
-
| Keeps an Error from another realm
|
|
65
|
-
| Rebuilds an error-like object with its name, message, code and stack | ✅
|
|
66
|
-
| Describes a circular object
|
|
67
|
-
| Describes an object containing a BigInt
|
|
68
|
-
| Converts an object whose `toJSON` and `toString` throw
|
|
69
|
-
| Converts a Proxy that throws from every trap
|
|
70
|
-
| Reads the message of an Error whose `message` getter throws
|
|
71
|
-
| Leaves the Error passed in unmodified
|
|
72
|
-
| Includes a cause that is not an Error
|
|
73
|
-
| Includes a cause from another realm
|
|
74
|
-
| Ends a circular cause chain
|
|
75
|
-
| Includes nested `AggregateError` entries
|
|
76
|
-
| Handles a 10,000-deep cause chain
|
|
60
|
+
| Case | unknown-error | normalize-exception | ensure-error | serialize-error | pony-cause | Hand-written |
|
|
61
|
+
| -------------------------------------------------------------------- | ------------- | ------------------- | ------------ | --------------- | ---------- | ------------ |
|
|
62
|
+
| Uses a thrown string as the message, unchanged | ✅ | ✅ | ❌ quotes it | ❌ prefixes it | — | ✅ |
|
|
63
|
+
| Converts a thrown `null` | ✅ | ✅ | ✅ | ✅ | — | ✅ |
|
|
64
|
+
| Keeps an Error from another realm | ✅ | ✅ | ❌ | ✅ | — | ❌ |
|
|
65
|
+
| Rebuilds an error-like object with its name, message, code and stack | ✅ | ✅ | ❌ | ✅ | — | ❌ |
|
|
66
|
+
| Describes a circular object | ✅ | ❌ | ❌ | ❌ | — | ❌ |
|
|
67
|
+
| Describes an object containing a BigInt | ✅ | ❌ | ❌ | ❌ | — | ❌ |
|
|
68
|
+
| Converts an object whose `toJSON` and `toString` throw | ✅ | ✅ | ✅ | ✅ | — | 💥 |
|
|
69
|
+
| Converts a Proxy that throws from every trap | ✅ | ✅ | 💥 | 💥 | — | 💥 |
|
|
70
|
+
| Reads the message of an Error whose `message` getter throws | ✅ | ✅ | 💥 | 💥 | — | 💥 |
|
|
71
|
+
| Leaves the Error passed in unmodified | ✅ | ❌ | ❌ | ✅ | — | ✅ |
|
|
72
|
+
| Includes a cause that is not an Error | ✅ | ✅ | ✅ | — | ❌ | — |
|
|
73
|
+
| Includes a cause from another realm | ✅ | ✅ | ❌ | — | ❌ | — |
|
|
74
|
+
| Ends a circular cause chain | ✅ | ✅ | ✅ | — | ✅ | — |
|
|
75
|
+
| Includes nested `AggregateError` entries | ✅ | ✅ | ✅ | — | ❌ | — |
|
|
76
|
+
| Handles a 10,000-deep cause chain | ✅ | 💥 | 💥 | — | 💥 | — |
|
|
77
77
|
|
|
78
78
|
## Design Decisions
|
|
79
79
|
|
package/dist/lib/safe.js
CHANGED
|
@@ -79,10 +79,7 @@ export const safeStringify = (value)=>{
|
|
|
79
79
|
const name = safeGet(value, 'name');
|
|
80
80
|
return typeof name === 'string' && name ? `[Function ${name}]` : '[Function (anonymous)]';
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
return stringifyObject(value);
|
|
84
|
-
}
|
|
85
|
-
return String(value);
|
|
82
|
+
return isObject(value) ? stringifyObject(value) : String(value);
|
|
86
83
|
};
|
|
87
84
|
|
|
88
85
|
//# sourceMappingURL=safe.js.map
|
package/dist/lib/walk.d.ts
CHANGED
|
@@ -50,8 +50,8 @@ export declare const walkErrors: (value: unknown, options?: WalkOptions) => Gene
|
|
|
50
50
|
/**
|
|
51
51
|
* Formats an error with every cause and `AggregateError` entry, indented by depth. Never throws.
|
|
52
52
|
*
|
|
53
|
-
* Each error is printed as its stack, with causes
|
|
54
|
-
*
|
|
53
|
+
* Each error is printed as its stack, with causes labeled `Caused by:` and `AggregateError` entries
|
|
54
|
+
* labeled `-`.
|
|
55
55
|
*
|
|
56
56
|
* @param value - a caught value
|
|
57
57
|
* @param options - {@link WalkOptions}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unknown-error",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Turn any caught `unknown` into a real Error without throwing, even for hostile values, other realms and deep or circular cause chains",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Turn any caught `unknown` into a real Error without throwing, even for hostile values, other realms and deep or circular cause chains.",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=22"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
9
|
"author": "JacobLey",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"url": "git+https://github.com/JacobLey/leyman.git",
|
|
14
14
|
"directory": "tools/unknown-error"
|
|
15
15
|
},
|
|
16
|
-
"homepage": "https://github.
|
|
16
|
+
"homepage": "https://jacobley.github.io/leyman/packages/unknown-error/",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/JacobLey/leyman/issues"
|
|
19
19
|
},
|
|
@@ -45,20 +45,19 @@
|
|
|
45
45
|
"#walk": "./dist/lib/walk.js"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
|
-
"@biomejs/biome": "^2.
|
|
48
|
+
"@biomejs/biome": "^2.6.4",
|
|
49
49
|
"@leyman/eslint-config": "^0.0.1",
|
|
50
50
|
"@leyman/expect": "^0.0.1",
|
|
51
51
|
"@swc/cli": "^0.8.1",
|
|
52
|
-
"@swc/core": "^1.16.
|
|
52
|
+
"@swc/core": "^1.16.20",
|
|
53
53
|
"@types/mocha": "^10.0.10",
|
|
54
|
-
"@types/node": "^
|
|
54
|
+
"@types/node": "^22.20.5",
|
|
55
55
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
56
56
|
"c8": "^12.0.0",
|
|
57
|
-
"eslint": "^10.
|
|
57
|
+
"eslint": "^10.13.0",
|
|
58
58
|
"expect-type": "^1.4.0",
|
|
59
|
-
"mocha": "^12.0.
|
|
60
|
-
"mocha-chain": "^1.0
|
|
61
|
-
"npm-barrelify": "npm:barrelify@^1.3.3",
|
|
59
|
+
"mocha": "^12.0.3",
|
|
60
|
+
"mocha-chain": "^1.1.0",
|
|
62
61
|
"typescript": "npm:@typescript/typescript6@^6.0.2"
|
|
63
62
|
}
|
|
64
63
|
}
|