errore 0.14.0 → 0.14.1

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 CHANGED
@@ -1,10 +1,10 @@
1
1
  # errore
2
2
 
3
- Type-safe errors as values for TypeScript. Like Go, but with full type inference.
3
+ Type-safe error handling for TypeScript. Return errors instead of throwing them — as a union type (`Error | T`), not a wrapper. TypeScript's type narrowing does the rest: forget to handle an error and your code won't compile.
4
4
 
5
5
  ## Why?
6
6
 
7
- Instead of wrapping values in a `Result<T, E>` type, functions simply return `E | T`. TypeScript's **type narrowing** handles the rest:
7
+ In Go, functions return errors as values instead of throwing exceptions. errore brings the same convention to TypeScript — but instead of a tuple with two separate variables, functions return a single `Error | T` union. You check `instanceof Error` instead of `err != nil`, and TypeScript narrows the type automatically. No wrapper types like `Result<T, E>`, no monads — just plain unions and `instanceof`:
8
8
 
9
9
  ```ts
10
10
  // Go-style: errors as values
@@ -17,7 +17,7 @@ if (user instanceof DbError) {
17
17
  console.error('DB failed:', user.reason)
18
18
  return
19
19
  }
20
- console.log(user.username) // user is User, fully narrowed
20
+ console.log(user.username) // user is User, fully narrowed
21
21
  ```
22
22
 
23
23
  ## Install
@@ -50,17 +50,18 @@ import * as errore from 'errore'
50
50
  // Define typed errors with $variable interpolation
51
51
  class NotFoundError extends errore.createTaggedError({
52
52
  name: 'NotFoundError',
53
- message: 'User $id not found'
53
+ message: 'User $id not found',
54
54
  }) {}
55
55
 
56
56
  class DbError extends errore.createTaggedError({
57
57
  name: 'DbError',
58
- message: 'Database query failed: $reason'
58
+ message: 'Database query failed: $reason',
59
59
  }) {}
60
60
 
61
61
  // Function returns Error | Value (no wrapper!)
62
62
  async function getUser(id: string): Promise<NotFoundError | DbError | User> {
63
- const result = await db.query(id)
63
+ const result = await db
64
+ .query(id)
64
65
  .catch((e) => new DbError({ reason: e.message, cause: e }))
65
66
 
66
67
  if (result instanceof Error) return result
@@ -74,9 +75,9 @@ const user = await getUser('123')
74
75
 
75
76
  if (user instanceof Error) {
76
77
  const message = errore.matchError(user, {
77
- NotFoundError: e => `User ${e.id} not found`,
78
- DbError: e => `Database error: ${e.reason}`,
79
- Error: e => `Unexpected error: ${e.message}`
78
+ NotFoundError: (e) => `User ${e.id} not found`,
79
+ DbError: (e) => `Database error: ${e.reason}`,
80
+ Error: (e) => `Unexpected error: ${e.message}`,
80
81
  })
81
82
  console.log(message)
82
83
  return
@@ -106,25 +107,25 @@ class AppError extends Error {
106
107
  class NotFoundError extends errore.createTaggedError({
107
108
  name: 'NotFoundError',
108
109
  message: '$resource not found',
109
- extends: AppError
110
+ extends: AppError,
110
111
  }) {}
111
112
 
112
113
  class ValidationError extends errore.createTaggedError({
113
114
  name: 'ValidationError',
114
115
  message: 'Invalid $field: $reason',
115
- extends: AppError
116
+ extends: AppError,
116
117
  }) {}
117
118
 
118
119
  class UnauthorizedError extends errore.createTaggedError({
119
120
  name: 'UnauthorizedError',
120
121
 
121
- extends: AppError
122
+ extends: AppError,
122
123
  }) {}
123
124
 
124
125
  // Service function
125
126
  async function updateUser(
126
127
  userId: string,
127
- data: { email?: string }
128
+ data: { email?: string },
128
129
  ): Promise<NotFoundError | ValidationError | UnauthorizedError | User> {
129
130
  const session = await getSession()
130
131
  if (!session) {
@@ -137,7 +138,10 @@ async function updateUser(
137
138
  }
138
139
 
139
140
  if (data.email && !isValidEmail(data.email)) {
140
- return new ValidationError({ field: 'email', reason: 'Invalid email format' })
141
+ return new ValidationError({
142
+ field: 'email',
143
+ reason: 'Invalid email format',
144
+ })
141
145
  }
142
146
 
143
147
  return db.users.update(userId, data)
@@ -168,21 +172,21 @@ import * as errore from 'errore'
168
172
  // Variables are extracted from the message and required in constructor
169
173
  class NotFoundError extends errore.createTaggedError({
170
174
  name: 'NotFoundError',
171
- message: 'User $id not found in $database'
175
+ message: 'User $id not found in $database',
172
176
  }) {}
173
177
 
174
178
  const err = new NotFoundError({ id: '123', database: 'users' })
175
- err.message // 'User 123 not found in users'
176
- err.id // '123'
177
- err.database // 'users'
178
- err._tag // 'NotFoundError'
179
+ err.message // 'User 123 not found in users'
180
+ err.id // '123'
181
+ err.database // 'users'
182
+ err._tag // 'NotFoundError'
179
183
 
180
184
  // Error without variables
181
185
  class EmptyError extends errore.createTaggedError({
182
186
  name: 'EmptyError',
183
- message: 'Something went wrong'
187
+ message: 'Something went wrong',
184
188
  }) {}
185
- new EmptyError() // no args required
189
+ new EmptyError() // no args required
186
190
 
187
191
  // Message omitted — caller provides it at construction time
188
192
  class GenericError extends errore.createTaggedError({
@@ -194,7 +198,7 @@ new GenericError({ message: 'caller decides the message' })
194
198
  // With cause for error chaining
195
199
  class WrapperError extends errore.createTaggedError({
196
200
  name: 'WrapperError',
197
- message: 'Failed to process $item'
201
+ message: 'Failed to process $item',
198
202
  }) {}
199
203
  new WrapperError({ item: 'data', cause: originalError })
200
204
 
@@ -206,12 +210,12 @@ class AppError extends Error {
206
210
  class HttpError extends errore.createTaggedError({
207
211
  name: 'HttpError',
208
212
  message: 'HTTP $status error',
209
- extends: AppError
213
+ extends: AppError,
210
214
  }) {}
211
215
 
212
216
  const err = new HttpError({ status: 404 })
213
- err.statusCode // 500 (inherited from AppError)
214
- err instanceof AppError // true
217
+ err.statusCode // 500 (inherited from AppError)
218
+ err instanceof AppError // true
215
219
  ```
216
220
 
217
221
  **Reserved variable names:** `$_tag`, `$name`, `$stack`, `$cause` cannot be used in message templates — they conflict with Error internals.
@@ -223,7 +227,7 @@ Wrap errors with additional context while **preserving the original error** via
223
227
  ```ts
224
228
  // Wrap with context, preserve original in cause
225
229
  async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
226
- const user = await getUser(id) // returns NotFoundError | User
230
+ const user = await getUser(id) // returns NotFoundError | User
227
231
 
228
232
  if (user instanceof Error) {
229
233
  return new ServiceError({ id, cause: user })
@@ -235,10 +239,10 @@ async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
235
239
  // Access original error via cause
236
240
  const result = await processUser('123')
237
241
  if (result instanceof Error) {
238
- console.log(result.message) // "Failed to process user 123"
242
+ console.log(result.message) // "Failed to process user 123"
239
243
 
240
244
  if (result.cause instanceof NotFoundError) {
241
- console.log(result.cause.id) // access original error's properties
245
+ console.log(result.cause.id) // access original error's properties
242
246
  }
243
247
  }
244
248
  ```
@@ -250,12 +254,12 @@ import * as errore from 'errore'
250
254
 
251
255
  class NotFoundError extends errore.createTaggedError({
252
256
  name: 'NotFoundError',
253
- message: 'User $id not found'
257
+ message: 'User $id not found',
254
258
  }) {}
255
259
 
256
260
  class ServiceError extends errore.createTaggedError({
257
261
  name: 'ServiceError',
258
- message: 'Failed to process user $id'
262
+ message: 'Failed to process user $id',
259
263
  }) {}
260
264
  ```
261
265
 
@@ -279,12 +283,12 @@ import * as errore from 'errore'
279
283
 
280
284
  class NotFoundError extends errore.createTaggedError({
281
285
  name: 'NotFoundError',
282
- message: 'User $id not found'
286
+ message: 'User $id not found',
283
287
  }) {}
284
288
 
285
289
  class ServiceError extends errore.createTaggedError({
286
290
  name: 'ServiceError',
287
- message: 'Failed to process user $id'
291
+ message: 'Failed to process user $id',
288
292
  }) {}
289
293
 
290
294
  // Deep chain: ServiceError -> NotFoundError
@@ -293,11 +297,11 @@ const service = new ServiceError({ id: '123', cause: notFound })
293
297
 
294
298
  // Instance method on tagged errors
295
299
  const found = service.findCause(NotFoundError)
296
- found?.id // '123' — type-safe access
300
+ found?.id // '123' — type-safe access
297
301
 
298
302
  // Standalone function for any Error
299
303
  const found2 = errore.findCause(service, NotFoundError)
300
- found2?.id // '123'
304
+ found2?.id // '123'
301
305
  ```
302
306
 
303
307
  This solves the problem where `result.cause instanceof MyError` only checks one level deep. `findCause` walks the entire chain:
@@ -309,10 +313,10 @@ const b = new ServiceError({ id: '123', cause: c })
309
313
  const a = new NotFoundError({ id: '456', cause: b })
310
314
 
311
315
  // Manual check only finds B
312
- a.cause instanceof DbError // false — only checks one level
316
+ a.cause instanceof DbError // false — only checks one level
313
317
 
314
318
  // findCause walks the full chain
315
- a.findCause(DbError) // finds C ✓
319
+ a.findCause(DbError) // finds C ✓
316
320
  ```
317
321
 
318
322
  Returns `undefined` if no matching ancestor is found. Safe against circular `.cause` references.
@@ -326,24 +330,26 @@ import * as errore from 'errore'
326
330
 
327
331
  class AppError extends Error {
328
332
  statusCode = 500
329
- toResponse() { return { error: this.message, code: this.statusCode } }
333
+ toResponse() {
334
+ return { error: this.message, code: this.statusCode }
335
+ }
330
336
  }
331
337
 
332
338
  class NotFoundError extends errore.createTaggedError({
333
339
  name: 'NotFoundError',
334
340
  message: 'Resource $id not found',
335
- extends: AppError
341
+ extends: AppError,
336
342
  }) {
337
343
  statusCode = 404
338
344
  }
339
345
 
340
346
  const err = new NotFoundError({ id: '123' })
341
- err instanceof NotFoundError // true
342
- err instanceof AppError // true
343
- err instanceof Error // true
347
+ err instanceof NotFoundError // true
348
+ err instanceof AppError // true
349
+ err instanceof Error // true
344
350
 
345
- err.statusCode // 404
346
- err.toResponse() // { error: 'Resource 123 not found', code: 404 }
351
+ err.statusCode // 404
352
+ err.toResponse() // { error: 'Resource 123 not found', code: 404 }
347
353
  ```
348
354
 
349
355
  ### Type Guards
@@ -373,21 +379,23 @@ const parsed = errore.try(() => JSON.parse(input))
373
379
  // Sync - with custom error type
374
380
  const parsed = errore.try({
375
381
  try: () => JSON.parse(input),
376
- catch: e => new ParseError({ reason: e.message, cause: e })
382
+ catch: (e) => new ParseError({ reason: e.message, cause: e }),
377
383
  })
378
384
 
379
385
  // Async — prefer .catch() for promises (no wrapper needed)
380
- const response = await fetch(url)
381
- .catch((e) => new NetworkError({ url, cause: e }))
386
+ const response = await fetch(url).catch(
387
+ (e) => new NetworkError({ url, cause: e }),
388
+ )
382
389
 
383
390
  // Async — errore.tryAsync also works, but .catch() is preferred
384
391
  const response = await errore.tryAsync({
385
392
  try: () => fetch(url),
386
- catch: e => new NetworkError({ url, cause: e })
393
+ catch: (e) => new NetworkError({ url, cause: e }),
387
394
  })
388
395
  ```
389
396
 
390
397
  > **Best practices for `try` / `tryAsync`:**
398
+ >
391
399
  > - **For async code, prefer `.catch()`** — `promise.catch((e) => new MyError({ cause: e }))` is simpler and avoids the wrapper. `errore.tryAsync` still works but `.catch()` is the idiomatic choice.
392
400
  > - **Use `errore.try` for sync code** — there's no equivalent of `.catch()` for synchronous throwing calls, so `errore.try(() => JSON.parse(input))` is the right tool.
393
401
  > - **Use as low as possible in the call stack** — only at boundaries with uncontrolled dependencies (third-party libs, `JSON.parse`, `fetch`, file I/O). Your own functions should return errors as values, never throw.
@@ -402,16 +410,16 @@ const response = await errore.tryAsync({
402
410
  import * as errore from 'errore'
403
411
 
404
412
  // Transform value (if not error)
405
- const name = errore.map(user, u => u.name)
413
+ const name = errore.map(user, (u) => u.name)
406
414
 
407
415
  // Transform error
408
- const appError = errore.mapError(dbError, e => new AppError({ cause: e }))
416
+ const appError = errore.mapError(dbError, (e) => new AppError({ cause: e }))
409
417
 
410
418
  // Chain operations
411
- const posts = errore.andThen(user, u => fetchPosts(u.id))
419
+ const posts = errore.andThen(user, (u) => fetchPosts(u.id))
412
420
 
413
421
  // Side effects
414
- const logged = errore.tap(user, u => console.log('Got user:', u.name))
422
+ const logged = errore.tap(user, (u) => console.log('Got user:', u.name))
415
423
  ```
416
424
 
417
425
  ### Resource Cleanup (defer)
@@ -458,8 +466,8 @@ You can also register existing `Disposable` objects directly:
458
466
 
459
467
  ```ts
460
468
  await using cleanup = new errore.AsyncDisposableStack()
461
- cleanup.use(dbConnection) // calls dbConnection[Symbol.dispose]() on exit
462
- cleanup.adopt(handle, (h) => h.close()) // custom cleanup for non-disposable values
469
+ cleanup.use(dbConnection) // calls dbConnection[Symbol.dispose]() on exit
470
+ cleanup.adopt(handle, (h) => h.close()) // custom cleanup for non-disposable values
463
471
  ```
464
472
 
465
473
  ### Extraction
@@ -478,8 +486,8 @@ const name = errore.unwrapOr(result, 'Anonymous')
478
486
 
479
487
  // Pattern match
480
488
  const message = errore.match(result, {
481
- ok: user => `Hello, ${user.name}`,
482
- err: error => `Failed: ${error.message}`
489
+ ok: (user) => `Hello, ${user.name}`,
490
+ err: (error) => `Failed: ${error.message}`,
483
491
  })
484
492
 
485
493
  // Split array into [successes, errors]
@@ -495,29 +503,33 @@ import * as errore from 'errore'
495
503
 
496
504
  class ValidationError extends errore.createTaggedError({
497
505
  name: 'ValidationError',
498
- message: 'Invalid $field'
506
+ message: 'Invalid $field',
499
507
  }) {}
500
508
 
501
509
  class NetworkError extends errore.createTaggedError({
502
510
  name: 'NetworkError',
503
- message: 'Failed to fetch $url'
511
+ message: 'Failed to fetch $url',
504
512
  }) {}
505
513
 
506
514
  // Exhaustive matching - Error handler is always required
507
515
  const message = errore.matchError(error, {
508
- ValidationError: e => `Invalid ${e.field}`,
509
- NetworkError: e => `Failed to fetch ${e.url}`,
510
- Error: e => `Unexpected: ${e.message}` // required fallback for plain Error
516
+ ValidationError: (e) => `Invalid ${e.field}`,
517
+ NetworkError: (e) => `Failed to fetch ${e.url}`,
518
+ Error: (e) => `Unexpected: ${e.message}`, // required fallback for plain Error
511
519
  })
512
- console.log(message) // side effects outside callbacks
520
+ console.log(message) // side effects outside callbacks
513
521
 
514
522
  // Partial matching with fallback
515
- const fallbackMsg = errore.matchErrorPartial(error, {
516
- ValidationError: e => `Invalid ${e.field}`
517
- }, e => `Unknown error: ${e.message}`)
523
+ const fallbackMsg = errore.matchErrorPartial(
524
+ error,
525
+ {
526
+ ValidationError: (e) => `Invalid ${e.field}`,
527
+ },
528
+ (e) => `Unknown error: ${e.message}`,
529
+ )
518
530
 
519
531
  // Type guards
520
- ValidationError.is(value) // specific class
532
+ ValidationError.is(value) // specific class
521
533
  ```
522
534
 
523
535
  ## How Type Safety Works
@@ -536,6 +548,7 @@ function example(result: NetworkError | User): string {
536
548
  ```
537
549
 
538
550
  This works because:
551
+
539
552
  1. `Error` is a built-in class TypeScript understands
540
553
  2. Custom error classes extend `Error`
541
554
  3. After an `instanceof Error` check, TS excludes all Error subtypes
@@ -549,7 +562,7 @@ import * as errore from 'errore'
549
562
 
550
563
  class NotFoundError extends errore.createTaggedError({
551
564
  name: 'NotFoundError',
552
- message: 'Resource $id not found'
565
+ message: 'Resource $id not found',
553
566
  }) {}
554
567
 
555
568
  // Result + Option in one natural type
@@ -563,7 +576,7 @@ const user = findUser('123')
563
576
 
564
577
  // Handle error first
565
578
  if (user instanceof Error) {
566
- return user.message // TypeScript: user is NotFoundError
579
+ return user.message // TypeScript: user is NotFoundError
567
580
  }
568
581
 
569
582
  // Handle null/missing case - use ?. and ?? naturally!
@@ -580,13 +593,14 @@ console.log(user.name)
580
593
 
581
594
  ### Why this is better than Rust/Zig
582
595
 
583
- | Language | Result + Option | Order matters? |
584
- |----------|-----------------|----------------|
585
- | Rust | `Result<Option<T>, E>` or `Option<Result<T, E>>` | Yes, must unwrap in order |
586
- | Zig | `!?T` (error union + optional) | Yes, specific syntax |
587
- | **errore** | `Error \| T \| null` | **No!** Check in any order |
596
+ | Language | Result + Option | Order matters? |
597
+ | ---------- | ------------------------------------------------ | -------------------------- |
598
+ | Rust | `Result<Option<T>, E>` or `Option<Result<T, E>>` | Yes, must unwrap in order |
599
+ | Zig | `!?T` (error union + optional) | Yes, specific syntax |
600
+ | **errore** | `Error \| T \| null` | **No!** Check in any order |
588
601
 
589
602
  With errore you **check in any order**:
603
+
590
604
  - Use `?.` and `??` naturally
591
605
  - Check `instanceof Error` or `=== null` in any order
592
606
  - No unwrapping ceremony
@@ -607,9 +621,9 @@ The compiler can't save you here. You can ignore `err` entirely and use `user` d
607
621
  With errore, **forgetting to check is impossible**:
608
622
 
609
623
  ```ts
610
- const user = await fetchUser(id) // type: NotFoundError | User
624
+ const user = await fetchUser(id) // type: NotFoundError | User
611
625
 
612
- console.log(user.id) // TS Error: Property 'id' does not exist on type 'NotFoundError'
626
+ console.log(user.id) // TS Error: Property 'id' does not exist on type 'NotFoundError'
613
627
  ```
614
628
 
615
629
  Since errore uses a **single union variable** instead of two separate values, TypeScript forces you to narrow the type before accessing value-specific properties. You literally cannot use the value without first doing an `instanceof Error` check.
@@ -622,12 +636,13 @@ There's still one case errore can't catch: **ignored return values**:
622
636
 
623
637
  ```ts
624
638
  // Oops! Completely ignoring the return value
625
- updateUser(id, data) // No error, but we should check!
639
+ updateUser(id, data) // No error, but we should check!
626
640
  ```
627
641
 
628
642
  For this, use **TypeScript's built-in checks** or a linter:
629
643
 
630
644
  **TypeScript `tsconfig.json`:**
645
+
631
646
  ```json
632
647
  {
633
648
  "compilerOptions": {
@@ -641,6 +656,7 @@ This catches unused variables, though not ignored return values directly.
641
656
  **oxlint `no-unused-expressions`:**
642
657
 
643
658
  `oxlint.json`:
659
+
644
660
  ```json
645
661
  {
646
662
  "rules": {
@@ -650,6 +666,7 @@ This catches unused variables, though not ignored return values directly.
650
666
  ```
651
667
 
652
668
  Or via CLI:
669
+
653
670
  ```bash
654
671
  oxlint --deny no-unused-expressions
655
672
  ```
@@ -660,14 +677,14 @@ Combined with errore's type safety, these tools give you near-complete protectio
660
677
 
661
678
  **Direct returns** vs wrapper methods:
662
679
 
663
- | Result Pattern | errore |
664
- |---------------|--------|
665
- | `Result.ok(value)` | just `return value` |
666
- | `Result.err(error)` | just `return error` |
667
- | `result.value` | direct access after guard |
668
- | `result.map(fn)` | `map(result, fn)` |
669
- | `Result<User, Error>` | `Error \| User` |
670
- | `Result<Option<T>, E>` | `Error \| T \| null` |
680
+ | Result Pattern | errore |
681
+ | ---------------------- | ------------------------- |
682
+ | `Result.ok(value)` | just `return value` |
683
+ | `Result.err(error)` | just `return error` |
684
+ | `result.value` | direct access after guard |
685
+ | `result.map(fn)` | `map(result, fn)` |
686
+ | `Result<User, Error>` | `Error \| User` |
687
+ | `Result<Option<T>, E>` | `Error \| T \| null` |
671
688
 
672
689
  ## Vs neverthrow / better-result
673
690
 
@@ -680,15 +697,15 @@ import { ok, err, Result } from 'neverthrow'
680
697
  function getUser(id: string): Result<User, NotFoundError> {
681
698
  const user = db.find(id)
682
699
  if (!user) return err(new NotFoundError({ id }))
683
- return ok(user) // must wrap
700
+ return ok(user) // must wrap
684
701
  }
685
702
 
686
703
  const result = getUser('123')
687
704
  if (result.isErr()) {
688
- console.log(result.error) // must unwrap
705
+ console.log(result.error) // must unwrap
689
706
  return
690
707
  }
691
- console.log(result.value.name) // must unwrap
708
+ console.log(result.value.name) // must unwrap
692
709
  ```
693
710
 
694
711
  ```ts
@@ -696,27 +713,27 @@ console.log(result.value.name) // must unwrap
696
713
  function getUser(id: string): User | NotFoundError {
697
714
  const user = db.find(id)
698
715
  if (!user) return new NotFoundError({ id })
699
- return user // just return
716
+ return user // just return
700
717
  }
701
718
 
702
719
  const user = getUser('123')
703
720
  if (user instanceof Error) {
704
- console.log(user) // it's already the error
721
+ console.log(user) // it's already the error
705
722
  return
706
723
  }
707
- console.log(user.name) // it's already the user
724
+ console.log(user.name) // it's already the user
708
725
  ```
709
726
 
710
727
  **The key insight**: `T | Error` already encodes success/failure. TypeScript's type narrowing does the rest. No wrapper needed.
711
728
 
712
- | Feature | neverthrow | errore |
713
- |---------|------------|--------|
714
- | Type-safe errors | ✓ | ✓ |
715
- | Exhaustive handling | ✓ | ✓ |
716
- | Works with null | `Result<T \| null, E>` | `T \| E \| null` |
717
- | Learning curve | New API (`ok`, `err`, `map`, `andThen`, ...) | Just `instanceof` |
718
- | Bundle size | ~3KB min | **~0 bytes** |
719
- | Interop | Requires wrapping/unwrapping at boundaries | Native TypeScript |
729
+ | Feature | neverthrow | errore |
730
+ | ------------------- | -------------------------------------------- | ----------------- |
731
+ | Type-safe errors | ✓ | ✓ |
732
+ | Exhaustive handling | ✓ | ✓ |
733
+ | Works with null | `Result<T \| null, E>` | `T \| E \| null` |
734
+ | Learning curve | New API (`ok`, `err`, `map`, `andThen`, ...) | Just `instanceof` |
735
+ | Bundle size | ~3KB min | **~0 bytes** |
736
+ | Interop | Requires wrapping/unwrapping at boundaries | Native TypeScript |
720
737
 
721
738
  neverthrow also requires an [eslint plugin](https://github.com/mdbetancourt/eslint-plugin-neverthrow) to catch unhandled results. With errore, TypeScript itself prevents you from using a value without checking the error first.
722
739
 
@@ -730,9 +747,9 @@ import { Effect, pipe } from 'effect'
730
747
 
731
748
  const program = pipe(
732
749
  fetchUser(id),
733
- Effect.flatMap(user => fetchPosts(user.id)),
734
- Effect.map(posts => posts.filter(p => p.published)),
735
- Effect.catchTag('NotFoundError', () => Effect.succeed([]))
750
+ Effect.flatMap((user) => fetchPosts(user.id)),
751
+ Effect.map((posts) => posts.filter((p) => p.published)),
752
+ Effect.catchTag('NotFoundError', () => Effect.succeed([])),
736
753
  )
737
754
 
738
755
  const result = await Effect.runPromise(program)
@@ -746,19 +763,19 @@ if (user instanceof Error) return []
746
763
  const posts = await fetchPosts(user.id)
747
764
  if (posts instanceof Error) return []
748
765
 
749
- return posts.filter(p => p.published)
766
+ return posts.filter((p) => p.published)
750
767
  ```
751
768
 
752
769
  Effect is powerful if you need its full feature set. But if you just want type-safe errors:
753
770
 
754
- | | Effect | errore |
755
- |-|--------|--------|
756
- | Learning curve | Steep (new paradigm) | Minimal (just `instanceof`) |
757
- | Codebase impact | Pervasive (everything becomes an Effect) | Surgical (adopt incrementally) |
758
- | Bundle size | ~50KB+ | **~0 bytes** |
771
+ | | Effect | errore |
772
+ | ---------------- | ------------------------------------------- | ----------------------------------- |
773
+ | Learning curve | Steep (new paradigm) | Minimal (just `instanceof`) |
774
+ | Codebase impact | Pervasive (everything becomes an Effect) | Surgical (adopt incrementally) |
775
+ | Bundle size | ~50KB+ | **~0 bytes** |
759
776
  | Resource cleanup | `Scope` + `addFinalizer` + `acquireRelease` | `using` + `DisposableStack.defer()` |
760
- | Cancellation | Fiber interruption model | Native `AbortController` |
761
- | Use case | Full FP framework | Just error handling |
777
+ | Cancellation | Fiber interruption model | Native `AbortController` |
778
+ | Use case | Full FP framework | Just error handling |
762
779
 
763
780
  **Use Effect** when you want dependency injection, structured concurrency, and the full functional programming experience.
764
781
 
package/dist/cli.js CHANGED
@@ -8,7 +8,7 @@ import path from 'node:path';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  const command = process.argv[2];
10
10
  if (command === 'skill') {
11
- const skillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'SKILL.md');
11
+ const skillPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills', 'errore', 'SKILL.md');
12
12
  const content = fs.readFileSync(skillPath, 'utf-8');
13
13
  process.stdout.write(content);
14
14
  }
package/dist/core.d.ts CHANGED
@@ -41,28 +41,43 @@ export declare function isOk<V>(value: V): value is Exclude<V, Error>;
41
41
  * // result: ParseError | unknown
42
42
  */
43
43
  export declare function tryFn<T>(fn: () => T): UnhandledError | T;
44
- export declare function tryFn<T, E extends Error>(opts: {
44
+ export declare function tryFn<T, E>(opts: {
45
45
  try: () => T;
46
46
  catch: (e: Error) => E;
47
47
  }): E | T;
48
48
  /**
49
49
  * Execute an async function and return either the value or an error.
50
50
  *
51
- * @overload Simple form - wraps exceptions in UnhandledError
52
- * @example
53
- * const result = await tryAsync(() => fetch(url).then(r => r.json()))
54
- * // result: UnhandledError | unknown
51
+ * @deprecated Use `.catch()` directly on the promise instead. It's simpler,
52
+ * composes naturally with async/await, and TypeScript infers the union
53
+ * automatically. `tryAsync` adds an unnecessary wrapper around what `.catch()`
54
+ * already does.
55
55
  *
56
- * @overload With custom catch - you control the error type
57
- * @example
56
+ * @example Migration from tryAsync to .catch()
57
+ * ```ts
58
+ * // Before (tryAsync):
58
59
  * const result = await tryAsync({
59
60
  * try: () => fetch(url),
60
- * catch: (e) => new NetworkError({ cause: e })
61
+ * catch: (e) => new NetworkError({ url, cause: e }),
61
62
  * })
62
- * // result: NetworkError | Response
63
+ *
64
+ * // After (.catch):
65
+ * const result = await fetch(url)
66
+ * .catch((e) => new NetworkError({ url, cause: e }))
67
+ * ```
68
+ *
69
+ * @example Simple form migration
70
+ * ```ts
71
+ * // Before:
72
+ * const result = await tryAsync(() => fetch(url).then(r => r.json()))
73
+ *
74
+ * // After:
75
+ * const result = await fetch(url).then(r => r.json())
76
+ * .catch((e) => new NetworkError({ url, cause: e }))
77
+ * ```
63
78
  */
64
79
  export declare function tryAsync<T>(fn: () => Promise<T>): Promise<UnhandledError | T>;
65
- export declare function tryAsync<T, E extends Error>(opts: {
80
+ export declare function tryAsync<T, E>(opts: {
66
81
  try: () => Promise<T>;
67
82
  catch: (e: Error) => E | Promise<E>;
68
83
  }): Promise<E | T>;
@@ -1 +1 @@
1
- {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAE5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,cAAc,GAAG,CAAC,CAAA;AACzD,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,SAAS,KAAK,EAAE,IAAI,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAA;CAAE,GAAG,CAAC,GAAG,CAAC,CAAA;AAyBhG;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,CAAC,CAAC,CAAA;AAC9E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,SAAS,KAAK,EAAE,IAAI,EAAE;IACjD,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAA;IACrB,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACpC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../src/core.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAE3C;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAE/D;AAED;;;;;;;;;GASG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,KAAK,IAAI,OAAO,CAAC,CAAC,EAAE,KAAK,CAAC,CAE5D;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,cAAc,GAAG,CAAC,CAAA;AACzD,wBAAgB,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE;IAChC,GAAG,EAAE,MAAM,CAAC,CAAA;IACZ,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,CAAA;CACvB,GAAG,CAAC,GAAG,CAAC,CAAA;AAyBT;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,QAAQ,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,cAAc,GAAG,CAAC,CAAC,CAAA;AAC9E,wBAAgB,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE;IACnC,GAAG,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,CAAA;IACrB,KAAK,EAAE,CAAC,CAAC,EAAE,KAAK,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;CACpC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"disposable.d.ts","sourceRoot":"","sources":["../src/disposable.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,KAAK,aAAa,GAAG,MAAM,IAAI,CAAA;AAC/B,KAAK,kBAAkB,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,eAAgB,YAAW,UAAU;;IAIhD;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAOrC;;;OAGG;IACH,GAAG,CAAC,CAAC,SAAS,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAOzD;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,CAAC;IAKpD;;;OAGG;IACH,IAAI,IAAI,eAAe;IAWvB;;;OAGG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAoBxB,OAAO,IAAI,IAAI;CAGhB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,oBAAqB,YAAW,eAAe;;IAI1D;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI;IAO1C;;;OAGG;IACH,GAAG,CAAC,CAAC,SAAS,eAAe,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAW3E;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;IAKpE;;;OAGG;IACH,IAAI,IAAI,oBAAoB;IAW5B;;;OAGG;IACG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBtC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;CAGpC"}
1
+ {"version":3,"file":"disposable.d.ts","sourceRoot":"","sources":["../src/disposable.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAQH,KAAK,aAAa,GAAG,MAAM,IAAI,CAAA;AAC/B,KAAK,kBAAkB,GAAG,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;AAEpD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAAa,eAAgB,YAAW,UAAU;;IAIhD;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAOrC;;;OAGG;IACH,GAAG,CAAC,CAAC,SAAS,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAOzD;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,CAAC;IAKpD;;;OAGG;IACH,IAAI,IAAI,eAAe;IAWvB;;;OAGG;IACH,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;IAoBxB,OAAO,IAAI,IAAI;CAGhB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,oBAAqB,YAAW,eAAe;;IAI1D;;OAEG;IACH,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED;;;OAGG;IACH,KAAK,CAAC,SAAS,EAAE,kBAAkB,GAAG,IAAI;IAO1C;;;OAGG;IACH,GAAG,CAAC,CAAC,SAAS,eAAe,GAAG,UAAU,GAAG,IAAI,GAAG,SAAS,EAAE,KAAK,EAAE,CAAC,GAAG,CAAC;IAa3E;;;OAGG;IACH,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;IAKpE;;;OAGG;IACH,IAAI,IAAI,oBAAoB;IAW5B;;;OAGG;IACG,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBtC,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;CAGpC"}