@polygonlabs/verror 0.0.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 +300 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @polygonlabs/verror
|
|
2
|
+
|
|
3
|
+
Structured error handling for TypeScript — cause chains, accumulated messages, and typed
|
|
4
|
+
`info` fields that survive serialisation.
|
|
5
|
+
|
|
6
|
+
Zero runtime dependencies. Pure ESM. Works in Node.js and browsers.
|
|
7
|
+
|
|
8
|
+
## Why not plain `Error`?
|
|
9
|
+
|
|
10
|
+
`new Error('query failed')` loses the context that caused it. You can attach a cause with
|
|
11
|
+
`new Error('query failed', { cause: dbErr })`, but the cause message disappears when you
|
|
12
|
+
log `err.message`, and `JSON.stringify(err)` produces `{}`.
|
|
13
|
+
|
|
14
|
+
`VError` solves both:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { VError } from '@polygonlabs/verror';
|
|
18
|
+
|
|
19
|
+
const dbErr = new Error('connection refused');
|
|
20
|
+
const err = new VError('query failed', { cause: dbErr });
|
|
21
|
+
|
|
22
|
+
err.message; // 'query failed: connection refused'
|
|
23
|
+
err.shortMessage; // 'query failed'
|
|
24
|
+
JSON.stringify(err); // { name, message, shortMessage, cause: { ... }, info: {} }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The full cause chain accumulates into `message` at construction time. `shortMessage` gives
|
|
28
|
+
you back the message you actually passed, without the appended chain.
|
|
29
|
+
|
|
30
|
+
## Classes
|
|
31
|
+
|
|
32
|
+
### `VError`
|
|
33
|
+
|
|
34
|
+
The base class. Wraps any `Error` as a cause and appends its full accumulated message.
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
const root = new Error('ECONNREFUSED');
|
|
38
|
+
const err = new VError('upstream timed out', { cause: root });
|
|
39
|
+
// err.message === 'upstream timed out: ECONNREFUSED'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Native `Error.cause` chains are walked automatically.** If the cause itself has a
|
|
43
|
+
`.cause` (ES2022 native cause), VError accumulates the whole chain — not just the
|
|
44
|
+
immediate cause message. This matters because third-party libraries and Node.js built-ins
|
|
45
|
+
increasingly set `.cause` directly:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
const root = new Error('ECONNREFUSED');
|
|
49
|
+
const native = new Error('fetch failed', { cause: root }); // native ES2022 cause
|
|
50
|
+
const err = new VError('could not load config', { cause: native });
|
|
51
|
+
|
|
52
|
+
err.message; // 'could not load config: fetch failed: ECONNREFUSED'
|
|
53
|
+
// ^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
54
|
+
// full native chain accumulated
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
`serializeError` and `toJSON` also follow native cause chains, so the full chain is
|
|
58
|
+
preserved when serialising errors you did not create.
|
|
59
|
+
|
|
60
|
+
Structured context goes in `info`. Unlike the message, `info` fields are machine-readable
|
|
61
|
+
and aggregate up the chain via `VError.info()`:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const err1 = new VError('db error', { info: { table: 'users' } });
|
|
65
|
+
const err2 = new VError('request failed', { cause: err1, info: { requestId: 'abc' } });
|
|
66
|
+
|
|
67
|
+
VError.info(err2); // { table: 'users', requestId: 'abc' }
|
|
68
|
+
// Closer errors win on key conflicts.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### `WError` (Wrapped Error)
|
|
72
|
+
|
|
73
|
+
Like `VError` but intentionally does **not** append the cause's message. Use it when you
|
|
74
|
+
want to report a high-level outcome without the noise of the full cause chain in `message`:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const dbErr = new VError('query failed: connection refused');
|
|
78
|
+
const appErr = new WError('could not load user', { cause: dbErr });
|
|
79
|
+
|
|
80
|
+
appErr.message; // 'could not load user' — cause message not appended
|
|
81
|
+
appErr.toString(); // 'WError: could not load user; caused by VError: query failed: ...'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The cause is still traversable — it just stays out of `message`. Use `WError` at service
|
|
85
|
+
boundaries where the cause is an internal detail that shouldn't leak into user-facing
|
|
86
|
+
strings.
|
|
87
|
+
|
|
88
|
+
### `MultiError`
|
|
89
|
+
|
|
90
|
+
Wraps multiple concurrent failures:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { MultiError, errorFromList } from '@polygonlabs/verror';
|
|
94
|
+
|
|
95
|
+
const results = await Promise.allSettled(tasks);
|
|
96
|
+
const errors = results.filter(r => r.status === 'rejected').map(r => r.reason);
|
|
97
|
+
const err = errorFromList(errors); // null | Error | MultiError
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`errorFromList` returns `null` for empty, the single error for one, and a `MultiError` for
|
|
101
|
+
many. `errorForEach` iterates either uniformly.
|
|
102
|
+
|
|
103
|
+
### HTTP errors
|
|
104
|
+
|
|
105
|
+
`HTTPError` base class and 17 concrete subclasses, each with a typed `statusCode`:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { NotFound, BadRequest, Forbidden } from '@polygonlabs/verror';
|
|
109
|
+
|
|
110
|
+
throw new NotFound('user not found', { cause: dbErr, info: { userId } });
|
|
111
|
+
// err.statusCode === 404
|
|
112
|
+
// err.toJSON() includes statusCode
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
All subclasses: `BadRequest` (400), `NotAuthenticated` (401), `PaymentError` (402),
|
|
116
|
+
`Forbidden` (403), `NotFound` (404), `MethodNotAllowed` (405), `NotAcceptable` (406),
|
|
117
|
+
`Timeout` (408), `Conflict` (409), `Gone` (410), `LengthRequired` (411),
|
|
118
|
+
`Unprocessable` (422), `TooManyRequests` (429), `GeneralError` (500),
|
|
119
|
+
`NotImplemented` (501), `BadGateway` (502), `Unavailable` (503).
|
|
120
|
+
|
|
121
|
+
## Serialisation
|
|
122
|
+
|
|
123
|
+
`JSON.stringify` on a plain `Error` produces `{}` — the message and cause are lost.
|
|
124
|
+
`VError.toJSON()` is called automatically by `JSON.stringify` and produces:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"name": "VError",
|
|
129
|
+
"message": "query failed: connection refused",
|
|
130
|
+
"shortMessage": "query failed",
|
|
131
|
+
"cause": { "name": "Error", "message": "connection refused", ... },
|
|
132
|
+
"info": {}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
To serialise an error you didn't create (e.g., from a `catch` block), use `serializeError`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { serializeError } from '@polygonlabs/verror';
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await fetch(url);
|
|
143
|
+
} catch (err) {
|
|
144
|
+
logger.error({ err: serializeError(err) }, 'fetch failed');
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`serializeError` handles any value: returns `undefined` for non-Errors, delegates to
|
|
149
|
+
`toJSON()` for VErrors, and produces the same JSON shape for plain Errors and native
|
|
150
|
+
`Error.cause` chains.
|
|
151
|
+
|
|
152
|
+
## Static helpers
|
|
153
|
+
|
|
154
|
+
All available as standalone imports or as static methods on `VError`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
import { cause, info, fullStack, findCauseByName, findCauseByType } from '@polygonlabs/verror';
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Helper | Purpose |
|
|
161
|
+
|--------|---------|
|
|
162
|
+
| `cause(err)` | Immediate cause, or `null` |
|
|
163
|
+
| `info(err)` | Merged info from the full chain |
|
|
164
|
+
| `fullStack(err)` | Stack trace with all chained causes appended |
|
|
165
|
+
| `findCauseByName(err, name)` | First cause whose `.name` matches |
|
|
166
|
+
| `findCauseByType(err, Type)` | First cause that is `instanceof Type` |
|
|
167
|
+
| `hasCauseWithName(err, name)` | Boolean shorthand for `findCauseByName` |
|
|
168
|
+
| `hasCauseWithType(err, Type)` | Boolean shorthand for `findCauseByType` |
|
|
169
|
+
| `errorFromList(errors)` | `null` / single error / `MultiError` |
|
|
170
|
+
| `errorForEach(err, fn)` | Iterate errors, unwrapping `MultiError` |
|
|
171
|
+
|
|
172
|
+
`findCauseByName` is useful even when you control the code. Once errors cross a
|
|
173
|
+
serialisation boundary, or when multiple copies of this package exist in a dependency
|
|
174
|
+
tree, `instanceof` checks fail. Name-based lookup works regardless.
|
|
175
|
+
|
|
176
|
+
## Subclassing
|
|
177
|
+
|
|
178
|
+
Define the error name at the class level with `override readonly name = '...' as const`.
|
|
179
|
+
String literals survive minification; `new.target.name` does not:
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
class DatabaseError extends VError {
|
|
183
|
+
override readonly name = 'DatabaseError' as const;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
class QueryTimeoutError extends DatabaseError {
|
|
187
|
+
override readonly name = 'QueryTimeoutError' as const;
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Unnamed subclasses inherit the parent's name — useful for internal specialisation without
|
|
192
|
+
a new name at the error boundary:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
class DatabaseError extends VError {
|
|
196
|
+
override readonly name = 'DatabaseError' as const;
|
|
197
|
+
constructor(message: string, options?: VErrorOptions) {
|
|
198
|
+
super(message, { ...options, info: { ...options?.info, layer: 'db' } });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Differences from the original verror
|
|
204
|
+
|
|
205
|
+
This package is inspired by [`verror`](https://github.com/TritonDataCenter/node-verror) and
|
|
206
|
+
[`@openagenda/verror`](https://github.com/OpenAgenda/verror) but diverges in several
|
|
207
|
+
places. If you are migrating from either, here is what changed.
|
|
208
|
+
|
|
209
|
+
### Constructor API
|
|
210
|
+
|
|
211
|
+
The original constructor puts options or cause first, message last, and supports
|
|
212
|
+
printf-style format strings as additional arguments:
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
// original verror — multiple overloads
|
|
216
|
+
new VError(message)
|
|
217
|
+
new VError(options, message)
|
|
218
|
+
new VError(cause, message)
|
|
219
|
+
new VError(cause, 'failed to %s %d items', 'update', 42)
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
This package uses a single consistent signature — message first, options second:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
// @polygonlabs/verror
|
|
226
|
+
new VError(message)
|
|
227
|
+
new VError(message, { cause, info })
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**No sprintf.** Messages are plain strings. Printf-style formatting mixes presentation
|
|
231
|
+
into the error layer, produces hard-to-test messages, and makes the TypeScript types
|
|
232
|
+
awkward. Format at the call site if you need interpolation.
|
|
233
|
+
|
|
234
|
+
### Name is a class-level declaration
|
|
235
|
+
|
|
236
|
+
The original allowed setting `name` per instance via the options object:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
new VError({ name: 'DatabaseError' }, 'query failed') // original only
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
This package does not support per-instance names. `name` is a property of the class,
|
|
243
|
+
not the instance — set it with `override readonly name`:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
class DatabaseError extends VError {
|
|
247
|
+
override readonly name = 'DatabaseError' as const;
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
This makes the error type visible in TypeScript, survives minification (string literals
|
|
252
|
+
are not mangled), and is consistent with how the rest of the JavaScript ecosystem
|
|
253
|
+
defines error names.
|
|
254
|
+
|
|
255
|
+
### `WError` requires a cause
|
|
256
|
+
|
|
257
|
+
In the original, `WError` can be constructed without a cause. Here, `cause` is required
|
|
258
|
+
in the options — a `WError` with no cause is just a `VError`, so the constraint is
|
|
259
|
+
enforced at the type level.
|
|
260
|
+
|
|
261
|
+
### Native `Error.cause` chain accumulation
|
|
262
|
+
|
|
263
|
+
The original predates ES2022 `Error.cause`. When wrapping a native error that itself
|
|
264
|
+
has a `.cause`, the original sees only the immediate cause message. This package walks
|
|
265
|
+
the full native chain:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
const root = new Error('ECONNREFUSED');
|
|
269
|
+
const native = new Error('upstream failed', { cause: root }); // ES2022 native cause
|
|
270
|
+
const err = new VError('request failed', { cause: native });
|
|
271
|
+
|
|
272
|
+
err.message; // 'request failed: upstream failed: ECONNREFUSED'
|
|
273
|
+
// ^^^^^^^^^^^^ walked by this package
|
|
274
|
+
// ^^^^^^^^^^^^^^^ only this in original verror
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`WError` still suppresses the entire cause chain from its own message, as expected.
|
|
278
|
+
|
|
279
|
+
### Serialisation
|
|
280
|
+
|
|
281
|
+
The original has no built-in serialisation support — `JSON.stringify(vErr)` produces
|
|
282
|
+
`{}` because `Error` properties are non-enumerable. This package implements `toJSON()`
|
|
283
|
+
on all error classes and provides `serializeError()` for errors you did not create.
|
|
284
|
+
The full cause chain is serialised recursively, including plain `Error` and native
|
|
285
|
+
`Error.cause` links.
|
|
286
|
+
|
|
287
|
+
### What was removed
|
|
288
|
+
|
|
289
|
+
- **`sprintf` / printf-style formatting** — use template literals instead
|
|
290
|
+
- **`name` in constructor options** — use `override readonly name` on the class
|
|
291
|
+
- **`strict` mode** — the option existed in some forks; not present here
|
|
292
|
+
- **`meta`** — appeared in some forks as a duplicate of `info`; not present here
|
|
293
|
+
|
|
294
|
+
### What was added
|
|
295
|
+
|
|
296
|
+
- **`serializeError()`** and `toJSON()` — full cause chain serialisation
|
|
297
|
+
- **Native `Error.cause` chain accumulation** — walks ES2022 cause chains
|
|
298
|
+
- **HTTP error classes** — `HTTPError` and 17 typed subclasses
|
|
299
|
+
- **Pure ESM, browser-compatible** — no `require`, no Node.js built-ins, zero
|
|
300
|
+
runtime dependencies
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@polygonlabs/verror",
|
|
3
|
+
"version": "0.0.0-0",
|
|
4
|
+
"description": "TypeScript-first, browser-friendly VError-inspired error handling library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [],
|
|
16
|
+
"dependencies": {},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@polygonlabs/apps-team-lint": "^1.0.0",
|
|
19
|
+
"@tsconfig/node-ts": "^23.0.0",
|
|
20
|
+
"@tsconfig/node24": "^24.0.0",
|
|
21
|
+
"@types/node": "^24.0.0",
|
|
22
|
+
"eslint": "^10.0.0",
|
|
23
|
+
"typescript": "^5.9.3",
|
|
24
|
+
"vitest": "^3.2.3"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"clean": "rm -rf node_modules dist *.tsbuildinfo"
|
|
32
|
+
}
|
|
33
|
+
}
|