@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.
Files changed (2) hide show
  1. package/README.md +485 -0
  2. 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
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerospin/error",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {