@zerospin/error 2.0.1 → 2.0.2
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 +485 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
# @zerospin/error
|
|
2
|
+
|
|
3
|
+
ZeroSpin Error provides a type-safe, structured error system for Effect-based applications. It extends Effect's `Data.TaggedError` to provide consistent error handling with serialization support, error codes, and optional metadata.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zerospin/error effect
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
### Creating Errors
|
|
14
|
+
|
|
15
|
+
`ZerospinError` can be created in two ways:
|
|
16
|
+
|
|
17
|
+
**Simple form (code only):**
|
|
18
|
+
```typescript
|
|
19
|
+
import { ZerospinError } from '@zerospin/error'
|
|
20
|
+
|
|
21
|
+
const error = new ZerospinError('my-error-code')
|
|
22
|
+
// Creates an error with code: 'my-error-code', message: 'my-error-code'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
**Full form (with metadata):**
|
|
26
|
+
```typescript
|
|
27
|
+
const error = new ZerospinError({
|
|
28
|
+
code: 'failed-to-fetch',
|
|
29
|
+
message: 'Failed to fetch data from server',
|
|
30
|
+
cause: originalError,
|
|
31
|
+
extra: { url: 'https://api.example.com' },
|
|
32
|
+
status: 500
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Using ZerospinError in Effect Contexts
|
|
37
|
+
|
|
38
|
+
### 1. Throwing Errors in Effect.gen
|
|
39
|
+
|
|
40
|
+
In `Effect.gen` functions, use `yield*` to throw errors:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { Effect } from 'effect'
|
|
44
|
+
import { ZerospinError } from '@zerospin/error'
|
|
45
|
+
|
|
46
|
+
const fetchData = Effect.gen(function* () {
|
|
47
|
+
const res = yield* Effect.promise(() => fetch('/api/data'))
|
|
48
|
+
|
|
49
|
+
if (res.status === 404) {
|
|
50
|
+
return yield* new ZerospinError({
|
|
51
|
+
code: 'resource-not-found',
|
|
52
|
+
message: 'Resource not found',
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return yield* res.json()
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Example from ZeroSpin:**
|
|
61
|
+
```typescript
|
|
62
|
+
// From packages/client/src/ZerospinClientAdapter.ts
|
|
63
|
+
export const ZerospinClientAdapter = Layer.effect(
|
|
64
|
+
ZerospinStorageAdapter,
|
|
65
|
+
Effect.fn('getClientAdapter')(function* () {
|
|
66
|
+
const isOPFSSupported = yield* OPFSAdapter.check().pipe(
|
|
67
|
+
Effect.either,
|
|
68
|
+
Effect.map((either) => Either.isRight(either))
|
|
69
|
+
)
|
|
70
|
+
if (isOPFSSupported) {
|
|
71
|
+
return OPFSAdapter
|
|
72
|
+
}
|
|
73
|
+
const isLocalStorageSupported = yield* LocalStorageAdapter.check().pipe(
|
|
74
|
+
Effect.either,
|
|
75
|
+
Effect.map((either) => Either.isRight(either))
|
|
76
|
+
)
|
|
77
|
+
if (isLocalStorageSupported) {
|
|
78
|
+
return LocalStorageAdapter
|
|
79
|
+
}
|
|
80
|
+
return yield* new ZerospinError({
|
|
81
|
+
code: 'no-adapter-available',
|
|
82
|
+
message: 'No adapter available',
|
|
83
|
+
})
|
|
84
|
+
})()
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 2. Mapping Errors with Effect.mapError
|
|
89
|
+
|
|
90
|
+
Transform errors from other Effect operations:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { Effect } from 'effect'
|
|
94
|
+
import { ZerospinError } from '@zerospin/error'
|
|
95
|
+
|
|
96
|
+
const parseJson = Effect.promise(() => res.json()).pipe(
|
|
97
|
+
Effect.mapError((error) => {
|
|
98
|
+
return new ZerospinError({
|
|
99
|
+
code: 'failed-to-parse-json',
|
|
100
|
+
message: 'Failed to parse JSON',
|
|
101
|
+
cause: error,
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Example from ZeroSpin:**
|
|
108
|
+
```typescript
|
|
109
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
110
|
+
const res = yield* Effect.promise(async () => {
|
|
111
|
+
return fetch(url, {
|
|
112
|
+
body: JSON.stringify(body),
|
|
113
|
+
headers: { 'Content-Type': 'application/json' },
|
|
114
|
+
method: 'POST',
|
|
115
|
+
}).catch((error: unknown) => {
|
|
116
|
+
throw new ZerospinError({
|
|
117
|
+
code: 'failed-to-fetch',
|
|
118
|
+
message: 'Failed to fetch',
|
|
119
|
+
cause: error,
|
|
120
|
+
})
|
|
121
|
+
})
|
|
122
|
+
}).pipe(
|
|
123
|
+
Effect.mapError((error) => {
|
|
124
|
+
return new ZerospinError({
|
|
125
|
+
code: 'failed-to-fetch',
|
|
126
|
+
message: 'Failed to fetch',
|
|
127
|
+
cause: error,
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3. Catching All Errors with Effect.catchAll
|
|
134
|
+
|
|
135
|
+
Handle any error and convert it to a ZerospinError:
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { Effect } from 'effect'
|
|
139
|
+
import { ZerospinError } from '@zerospin/error'
|
|
140
|
+
|
|
141
|
+
const safeOperation = someEffect.pipe(
|
|
142
|
+
Effect.catchAll((error) => {
|
|
143
|
+
return new ZerospinError({
|
|
144
|
+
code: 'operation-failed',
|
|
145
|
+
message: error.message,
|
|
146
|
+
cause: error,
|
|
147
|
+
})
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Example from ZeroSpin:**
|
|
153
|
+
```typescript
|
|
154
|
+
// From packages/zerospin/src/batch/makeSafe.ts
|
|
155
|
+
const safe = yield* validateUnknown({
|
|
156
|
+
onExcessProperty: 'preserve',
|
|
157
|
+
schema,
|
|
158
|
+
value: command,
|
|
159
|
+
}).pipe(
|
|
160
|
+
Effect.catchAll((error) => {
|
|
161
|
+
return new ZerospinError({
|
|
162
|
+
code: 'failed-to-validate-command',
|
|
163
|
+
message: error.message,
|
|
164
|
+
cause: error,
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### 4. Handling Promise Errors
|
|
171
|
+
|
|
172
|
+
When working with promises, catch errors and throw ZerospinError:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { Effect } from 'effect'
|
|
176
|
+
import { ZerospinError } from '@zerospin/error'
|
|
177
|
+
|
|
178
|
+
const fetchData = Effect.promise(async () => {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch('/api/data')
|
|
181
|
+
return await response.json()
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new ZerospinError({
|
|
184
|
+
code: 'fetch-failed',
|
|
185
|
+
message: 'Failed to fetch data',
|
|
186
|
+
cause: error,
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Example from ZeroSpin:**
|
|
193
|
+
```typescript
|
|
194
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
195
|
+
const res = yield* Effect.promise(async () => {
|
|
196
|
+
return fetch(url, {
|
|
197
|
+
body: JSON.stringify(body),
|
|
198
|
+
headers: { 'Content-Type': 'application/json' },
|
|
199
|
+
method: 'POST',
|
|
200
|
+
}).catch((error: unknown) => {
|
|
201
|
+
throw new ZerospinError({
|
|
202
|
+
code: 'failed-to-fetch',
|
|
203
|
+
message: 'Failed to fetch',
|
|
204
|
+
cause: error,
|
|
205
|
+
})
|
|
206
|
+
})
|
|
207
|
+
})
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 5. Schema Validation Errors
|
|
211
|
+
|
|
212
|
+
Convert schema validation errors to ZerospinError:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { Effect, Schema } from 'effect'
|
|
216
|
+
import { ZerospinError } from '@zerospin/error'
|
|
217
|
+
|
|
218
|
+
const validateData = Schema.decode(Schema.String)(value).pipe(
|
|
219
|
+
Effect.mapError((error) => {
|
|
220
|
+
return new ZerospinError({
|
|
221
|
+
code: 'validation-failed',
|
|
222
|
+
message: error.message,
|
|
223
|
+
cause: error,
|
|
224
|
+
})
|
|
225
|
+
})
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Example from ZeroSpin:**
|
|
230
|
+
```typescript
|
|
231
|
+
// From packages/zerospin/src/utils/encodeUnknown.ts
|
|
232
|
+
export const encodeUnknown = Effect.fn('encodeUnknown')(<
|
|
233
|
+
DECODED,
|
|
234
|
+
ENCODED,
|
|
235
|
+
>(props: {
|
|
236
|
+
schema: Schema.Schema<DECODED, ENCODED>
|
|
237
|
+
value: unknown
|
|
238
|
+
errorMessage?: string
|
|
239
|
+
}): Effect.Effect<ENCODED, ZerospinError<'failed-to-encode-unknown'>> => {
|
|
240
|
+
const { errorMessage, schema, value } = props
|
|
241
|
+
|
|
242
|
+
return Schema.encodeUnknown(schema)(value).pipe(
|
|
243
|
+
Effect.mapError((error) => {
|
|
244
|
+
return new ZerospinError({
|
|
245
|
+
code: 'failed-to-encode-unknown',
|
|
246
|
+
message: errorMessage ?? error.message,
|
|
247
|
+
cause: error,
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
)
|
|
251
|
+
})
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**Another example:**
|
|
255
|
+
```typescript
|
|
256
|
+
// From packages/zerospin/src/contract/makeContract.ts
|
|
257
|
+
return validateUnknown({
|
|
258
|
+
onExcessProperty: 'error',
|
|
259
|
+
schema: resultsSchema,
|
|
260
|
+
value: results,
|
|
261
|
+
}).pipe(
|
|
262
|
+
Effect.mapError((error) => {
|
|
263
|
+
return new ZerospinError({
|
|
264
|
+
code: 'failed-to-validate-results',
|
|
265
|
+
message: error.message,
|
|
266
|
+
cause: error,
|
|
267
|
+
})
|
|
268
|
+
})
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 6. Deserializing Errors from JSON
|
|
273
|
+
|
|
274
|
+
When receiving errors from APIs or serialized sources:
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
import { ZerospinError } from '@zerospin/error'
|
|
278
|
+
|
|
279
|
+
// From packages/client/src/makeZerospinFetchFrontendApi.ts
|
|
280
|
+
const payload = yield* Effect.promise(() => {
|
|
281
|
+
return res.json() as Promise<IAnyErrorJson | IJson>
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
if (res.status === 200) {
|
|
285
|
+
return payload as IJson
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Deserialize error from JSON
|
|
289
|
+
return yield* new ZerospinError(payload as IAnyErrorJson)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 7. Error Handling in Route Handlers
|
|
293
|
+
|
|
294
|
+
Handle errors at the boundary and serialize for HTTP responses:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { Effect, Exit, Cause } from 'effect'
|
|
298
|
+
import { ZerospinError } from '@zerospin/error'
|
|
299
|
+
import { NextResponse } from 'next/server'
|
|
300
|
+
|
|
301
|
+
const exit = await Effect.runPromiseExit(myEffect)
|
|
302
|
+
|
|
303
|
+
return Exit.match(exit, {
|
|
304
|
+
onFailure: (cause) => {
|
|
305
|
+
console.error(Cause.pretty(cause))
|
|
306
|
+
if (Cause.isFailType(cause)) {
|
|
307
|
+
const error = cause.error as ZerospinError
|
|
308
|
+
return NextResponse.json(error.serialize(), {
|
|
309
|
+
status: error.status ?? 400,
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
return NextResponse.json(
|
|
313
|
+
new ZerospinError({
|
|
314
|
+
code: 'unexpected-error',
|
|
315
|
+
message: Cause.pretty(cause),
|
|
316
|
+
}).serialize(),
|
|
317
|
+
{ status: 500 }
|
|
318
|
+
)
|
|
319
|
+
},
|
|
320
|
+
onSuccess: (value) => {
|
|
321
|
+
return NextResponse.json(value)
|
|
322
|
+
},
|
|
323
|
+
})
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Example from ZeroSpin:**
|
|
327
|
+
```typescript
|
|
328
|
+
// From packages/zerospin/src/rpc/makeNextRoute.ts
|
|
329
|
+
return Exit.match(exit, {
|
|
330
|
+
onFailure: (cause) => {
|
|
331
|
+
console.error(Cause.pretty(cause))
|
|
332
|
+
if (Cause.isFailType(cause)) {
|
|
333
|
+
const error = cause.error as ZerospinError
|
|
334
|
+
return NextResponse.json(error.serialize(), {
|
|
335
|
+
status: error.status ?? 400,
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
return NextResponse.json(
|
|
339
|
+
new ZerospinError({
|
|
340
|
+
code: 'unexpected-error',
|
|
341
|
+
message: Cause.pretty(cause),
|
|
342
|
+
}).serialize(),
|
|
343
|
+
{ status: 500 }
|
|
344
|
+
)
|
|
345
|
+
},
|
|
346
|
+
onSuccess: (value) => {
|
|
347
|
+
return NextResponse.json(value)
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
## Error Properties
|
|
353
|
+
|
|
354
|
+
### Error Structure
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
interface IError<T extends string = string, E = unknown> {
|
|
358
|
+
code: T // Error code identifier
|
|
359
|
+
status: null | number // HTTP status code (optional)
|
|
360
|
+
cause?: unknown // Original error that caused this
|
|
361
|
+
extra?: E // Additional typed metadata
|
|
362
|
+
message?: string // Human-readable message
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Type Safety with Error Codes
|
|
367
|
+
|
|
368
|
+
You can create type-safe error codes:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
type MyErrorCodes =
|
|
372
|
+
| 'failed-to-fetch'
|
|
373
|
+
| 'validation-failed'
|
|
374
|
+
| 'resource-not-found'
|
|
375
|
+
|
|
376
|
+
const error: ZerospinError<MyErrorCodes> = new ZerospinError({
|
|
377
|
+
code: 'failed-to-fetch', // TypeScript will validate this
|
|
378
|
+
message: 'Failed to fetch',
|
|
379
|
+
})
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Utility Methods
|
|
383
|
+
|
|
384
|
+
### Checking if Something is a ZerospinError
|
|
385
|
+
|
|
386
|
+
```typescript
|
|
387
|
+
import { ZerospinError } from '@zerospin/error'
|
|
388
|
+
|
|
389
|
+
if (ZerospinError.isZerospinError(error)) {
|
|
390
|
+
console.log(error.code)
|
|
391
|
+
console.log(error.message)
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
**Example from ZeroSpin:**
|
|
396
|
+
```typescript
|
|
397
|
+
// From packages/zerospin/src/ZerospinErrorLayer.ts
|
|
398
|
+
Exit.mapError(exit, (error) => {
|
|
399
|
+
if (ZerospinError.isZerospinError(error)) {
|
|
400
|
+
if (error.cause) {
|
|
401
|
+
error.message += ` ${JSON.stringify(error.cause, null, 2)}`
|
|
402
|
+
}
|
|
403
|
+
if (error.extra) {
|
|
404
|
+
error.message += ` ${JSON.stringify(error.extra, null, 2)}`
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return error
|
|
408
|
+
})
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Serializing Errors
|
|
412
|
+
|
|
413
|
+
Serialize errors for transmission over networks or storage:
|
|
414
|
+
|
|
415
|
+
```typescript
|
|
416
|
+
const error = new ZerospinError({
|
|
417
|
+
code: 'my-error',
|
|
418
|
+
message: 'Something went wrong',
|
|
419
|
+
extra: { userId: '123' },
|
|
420
|
+
status: 400,
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
// Serialize full error
|
|
424
|
+
const json = error.serialize()
|
|
425
|
+
|
|
426
|
+
// Serialize without certain fields
|
|
427
|
+
const jsonWithoutExtra = error.serialize(['extra'])
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
## Best Practices
|
|
431
|
+
|
|
432
|
+
1. **Always provide meaningful error codes**: Use descriptive, kebab-case codes like `'failed-to-fetch'` instead of generic ones.
|
|
433
|
+
|
|
434
|
+
2. **Preserve original errors**: Use the `cause` property to maintain error chains:
|
|
435
|
+
```typescript
|
|
436
|
+
new ZerospinError({
|
|
437
|
+
code: 'operation-failed',
|
|
438
|
+
message: 'Operation failed',
|
|
439
|
+
cause: originalError, // Preserve the original error
|
|
440
|
+
})
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
3. **Use typed extra data**: Leverage TypeScript generics for type-safe metadata:
|
|
444
|
+
```typescript
|
|
445
|
+
new ZerospinError<ErrorCode, { userId: string; action: string }>({
|
|
446
|
+
code: 'permission-denied',
|
|
447
|
+
extra: { userId: '123', action: 'delete' },
|
|
448
|
+
})
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
4. **Handle at boundaries**: Convert to ZerospinError at Effect boundaries (promises, HTTP handlers, etc.) and let them propagate through your Effect pipeline.
|
|
452
|
+
|
|
453
|
+
5. **Serialize for APIs**: Use `serialize()` when sending errors over HTTP or storing them.
|
|
454
|
+
|
|
455
|
+
## API Reference
|
|
456
|
+
|
|
457
|
+
### ZerospinError Class
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
class ZerospinError<T extends string = never, E = unknown>
|
|
461
|
+
extends Data.TaggedError('ZerospinError')<IError<T, E>>
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Constructor:**
|
|
465
|
+
- `new ZerospinError(code: string)` - Simple form
|
|
466
|
+
- `new ZerospinError(props: IProps<T, E>)` - Full form
|
|
467
|
+
|
|
468
|
+
**Static Methods:**
|
|
469
|
+
- `ZerospinError.isZerospinError(data: unknown): data is ZerospinError` - Type guard
|
|
470
|
+
- `ZerospinError.makeZerospinErrorJson(props): IAnyErrorJson` - Create JSON representation
|
|
471
|
+
|
|
472
|
+
**Instance Methods:**
|
|
473
|
+
- `serialize(omit?: string[]): IAnyErrorJson` - Serialize to JSON
|
|
474
|
+
|
|
475
|
+
### Types
|
|
476
|
+
|
|
477
|
+
```typescript
|
|
478
|
+
type IAnyError = ZerospinError<string>
|
|
479
|
+
type IAnyErrorJson = Brand.Brand<'ZerospinErrorJson'> & {
|
|
480
|
+
code: string
|
|
481
|
+
extra: unknown
|
|
482
|
+
message: string
|
|
483
|
+
status: null | number
|
|
484
|
+
}
|
|
485
|
+
```
|