@nice-code/error 0.2.8 → 0.2.10
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 +237 -5
- package/build/index.js +19 -2891
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,237 @@
|
|
|
1
|
-
# @nice-code/error
|
|
2
|
-
|
|
3
|
-
Typed, serializable errors with domain hierarchies, pattern matching, and safe transport across API boundaries.
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# @nice-code/error
|
|
2
|
+
|
|
3
|
+
Typed, serializable errors with domain hierarchies, pattern matching, and safe transport across API boundaries.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @nice-code/error
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Core concepts
|
|
12
|
+
|
|
13
|
+
- **Domain** — a named group of related errors (`err_user_auth`, `err_payment`)
|
|
14
|
+
- **Schema** — maps error id strings to message, HTTP status, and optional typed context
|
|
15
|
+
- **Error id** — identifies what went wrong within a domain; multiple ids can be active on one error
|
|
16
|
+
- **Context** — structured data attached to an id (e.g. `{ username: string }`)
|
|
17
|
+
- **Hydration** — deserializing context from a JSON round-trip back to the original typed value
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Defining an error domain
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { defineNiceError, err } from "@nice-code/error";
|
|
25
|
+
|
|
26
|
+
export const err_auth = defineNiceError({
|
|
27
|
+
domain: "err_auth",
|
|
28
|
+
schema: {
|
|
29
|
+
// No context — second arg to fromId is not accepted
|
|
30
|
+
account_locked: err({
|
|
31
|
+
message: "Account is locked",
|
|
32
|
+
httpStatusCode: 403,
|
|
33
|
+
}),
|
|
34
|
+
|
|
35
|
+
// Required context — second arg to fromId is required
|
|
36
|
+
invalid_credentials: err<{ username: string }>({
|
|
37
|
+
message: ({ username }) => `Invalid credentials for: ${username}`,
|
|
38
|
+
httpStatusCode: 401,
|
|
39
|
+
context: { required: true },
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
// Optional context
|
|
43
|
+
rate_limited: err<{ retryAfter: number }>({
|
|
44
|
+
message: (ctx) => ctx ? `Retry after ${ctx.retryAfter}s` : "Rate limited",
|
|
45
|
+
httpStatusCode: 429,
|
|
46
|
+
context: {},
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
} as const);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### `err()` — schema entry helper
|
|
53
|
+
|
|
54
|
+
| Schema entry | `fromId("id")` | `fromId("id", ctx)` |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `err()` / `err({ message, httpStatusCode })` | ✓ | ✗ |
|
|
57
|
+
| `err<C>({ context: {} })` | ✓ optional | ✓ optional |
|
|
58
|
+
| `err<C>({ context: { required: true } })` | ✗ | ✓ required |
|
|
59
|
+
|
|
60
|
+
### Custom serialization (non-JSON-safe context)
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
fs_error: err<{ cause: NodeJS.ErrnoException }>({
|
|
64
|
+
message: ({ cause }) => `FS error: ${cause.message}`,
|
|
65
|
+
context: {
|
|
66
|
+
required: true,
|
|
67
|
+
serialization: {
|
|
68
|
+
toJsonSerializable: ({ cause }) => ({ code: cause.code, message: cause.message }),
|
|
69
|
+
fromJsonSerializable: (obj) => ({ cause: Object.assign(new Error(obj.message), obj) }),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}),
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Creating errors
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// Single id — context required per schema
|
|
81
|
+
const err1 = err_auth.fromId("invalid_credentials", { username: "alice" });
|
|
82
|
+
|
|
83
|
+
// Multi-id in one error
|
|
84
|
+
const err2 = err_auth.fromContext({
|
|
85
|
+
invalid_credentials: { username: "bob" },
|
|
86
|
+
rate_limited: { retryAfter: 30 },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Chain additional ids after construction
|
|
90
|
+
const err3 = err_auth.fromId("account_locked").addId("rate_limited");
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Reading errors
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Check and narrow type
|
|
99
|
+
if (err1.hasId("invalid_credentials")) {
|
|
100
|
+
const { username } = err1.getContext("invalid_credentials"); // typed
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check multiple ids at once
|
|
104
|
+
if (err2.hasOneOfIds(["invalid_credentials", "rate_limited"])) {
|
|
105
|
+
// ACTIVE_IDS narrowed to that subset
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Pattern match — calls first matching handler, returns its result
|
|
109
|
+
import { matchFirst } from "@nice-code/error";
|
|
110
|
+
|
|
111
|
+
const message = matchFirst(err1, {
|
|
112
|
+
invalid_credentials: ({ username }) => `Wrong password for ${username}`,
|
|
113
|
+
account_locked: () => "Account locked",
|
|
114
|
+
_: () => "Unknown auth error",
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Domain hierarchies
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
const err_app = defineNiceError({ domain: "err_app", schema: {} } as const);
|
|
124
|
+
|
|
125
|
+
const err_auth = err_app.createChildDomain({
|
|
126
|
+
domain: "err_auth",
|
|
127
|
+
schema: { /* ... */ },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const err_auth_registration = err_auth.createChildDomain({
|
|
131
|
+
domain: "err_auth_registration",
|
|
132
|
+
schema: { /* ... */ },
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Ancestry checks
|
|
136
|
+
err_app.isParentOf(err_auth_registration); // true
|
|
137
|
+
err_auth.isParentOf(err_auth_registration); // true
|
|
138
|
+
err_auth_registration.isParentOf(err_auth); // false
|
|
139
|
+
|
|
140
|
+
// Type guard — exact domain match only
|
|
141
|
+
err_auth.isExact(someError); // true only for err_auth domain
|
|
142
|
+
err_auth.isThisOrChild(someError); // true for err_auth and all children
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Serialization & transport
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// Serialize to a plain JSON object (safe to send over HTTP)
|
|
151
|
+
const json = error.toJsonObject();
|
|
152
|
+
const str = error.toJsonString();
|
|
153
|
+
const response = error.toHttpResponse(); // Response with correct HTTP status
|
|
154
|
+
|
|
155
|
+
// On the receiving side — reconstruct from anything
|
|
156
|
+
import { castNiceError } from "@nice-code/error";
|
|
157
|
+
|
|
158
|
+
const caught = castNiceError(unknownValue); // always returns NiceError
|
|
159
|
+
|
|
160
|
+
if (err_auth.isExact(caught)) {
|
|
161
|
+
const hydrated = err_auth.hydrate(caught); // deserializes context
|
|
162
|
+
const { username } = hydrated.getContext("invalid_credentials");
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`castNiceError` handles: `NiceError` instances, serialized JSON objects, native `Error`, error-like objects, nullish values, and primitives.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Error handling with handlers
|
|
171
|
+
|
|
172
|
+
### Functional style (composable)
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
import { forDomain, forId, forIds } from "@nice-code/error";
|
|
176
|
+
|
|
177
|
+
const handled = error.handleWithSync([
|
|
178
|
+
forId(err_auth, "invalid_credentials", (h) => {
|
|
179
|
+
const { username } = h.getContext("invalid_credentials");
|
|
180
|
+
return res.status(401).json({ error: `Bad credentials for ${username}` });
|
|
181
|
+
}),
|
|
182
|
+
forIds(err_auth, ["account_locked", "rate_limited"], (h) => {
|
|
183
|
+
return res.status(h.httpStatusCode).json({ error: h.message });
|
|
184
|
+
}),
|
|
185
|
+
forDomain(err_payment, (h) => {
|
|
186
|
+
return res.status(500).json({ error: "Payment failed" });
|
|
187
|
+
}),
|
|
188
|
+
]);
|
|
189
|
+
|
|
190
|
+
// Async handlers
|
|
191
|
+
await error.handleWithAsync([
|
|
192
|
+
forDomain(err_payment, async (h) => {
|
|
193
|
+
await db.logFailure(h.message);
|
|
194
|
+
await notify(h.toJsonObject());
|
|
195
|
+
}),
|
|
196
|
+
]);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Class-based style (reusable handler)
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { NiceErrorHandler } from "@nice-code/error";
|
|
203
|
+
|
|
204
|
+
const handler = new NiceErrorHandler()
|
|
205
|
+
.forId(err_auth, "invalid_credentials", (h) => "unauthorized")
|
|
206
|
+
.forDomain(err_payment, (h) => "payment_error")
|
|
207
|
+
.setDefaultHandler((e) => "unknown_error");
|
|
208
|
+
|
|
209
|
+
handler.handleErrorWithPromiseInspection(error);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Utility types
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
import type { InferNiceError, InferNiceErrorHydrated } from "@nice-code/error";
|
|
218
|
+
|
|
219
|
+
type TAuthError = InferNiceError<typeof err_auth>;
|
|
220
|
+
// → NiceError<{ domain: "err_auth"; ... }, keyof schema>
|
|
221
|
+
|
|
222
|
+
type TAuthErrorHydrated = InferNiceErrorHydrated<typeof err_auth>;
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## Packing for transport
|
|
228
|
+
|
|
229
|
+
Useful when errors must travel through systems that don't preserve custom properties (e.g. some RPC layers).
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { EErrorPackType } from "@nice-code/error";
|
|
233
|
+
|
|
234
|
+
error.pack(EErrorPackType.msg_pack); // embeds JSON in error.message
|
|
235
|
+
error.pack(EErrorPackType.cause_pack); // embeds JSON in error.cause
|
|
236
|
+
error.unpack(); // restore original
|
|
237
|
+
```
|