@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.
Files changed (2) hide show
  1. package/README.md +300 -0
  2. 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
+ }