errore 0.9.0 → 0.11.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.
package/README.md CHANGED
@@ -4,13 +4,20 @@ Type-safe errors as values for TypeScript. Like Go, but with full type inference
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
+ Instead of wrapping values in a `Result<T, E>` type, functions simply return `E | T`. TypeScript's **type narrowing** handles the rest:
8
8
 
9
9
  ```ts
10
10
  // Go-style: errors as values
11
- const user = await fetchUser(id)
12
- if (user instanceof Error) return user // TypeScript narrows type
13
- console.log(user.name) // user is now User, not Error | User
11
+ const user = await getUser(id)
12
+ if (user instanceof NotFoundError) {
13
+ console.error('Missing:', user.id)
14
+ return
15
+ }
16
+ if (user instanceof DbError) {
17
+ console.error('DB failed:', user.reason)
18
+ return
19
+ }
20
+ console.log(user.username) // user is User, fully narrowed
14
21
  ```
15
22
 
16
23
  ## Install
@@ -21,6 +28,8 @@ npm install errore
21
28
 
22
29
  ## Quick Start
23
30
 
31
+ Define typed errors with **variable interpolation** and return **Error or Value** directly:
32
+
24
33
  ```ts
25
34
  import * as errore from 'errore'
26
35
 
@@ -67,7 +76,7 @@ console.log(user.name)
67
76
 
68
77
  ## Example: API Error Handling
69
78
 
70
- A complete example with custom base class, HTTP status codes, and error reporting:
79
+ A complete example with **custom base class** and HTTP status codes:
71
80
 
72
81
  ```ts
73
82
  import * as errore from 'errore'
@@ -139,7 +148,7 @@ app.post('/users/:id', async (req, res) => {
139
148
 
140
149
  ### createTaggedError
141
150
 
142
- Create typed errors with `$variable` interpolation in the message:
151
+ Create typed errors with **variable interpolation** in the message:
143
152
 
144
153
  ```ts
145
154
  import * as errore from 'errore'
@@ -186,8 +195,140 @@ err.statusCode // 500 (inherited from AppError)
186
195
  err instanceof AppError // true
187
196
  ```
188
197
 
198
+ ### Error Wrapping and Context
199
+
200
+ Wrap errors with additional context while **preserving the original error** via `cause`:
201
+
202
+ ```ts
203
+ // Wrap with context, preserve original in cause
204
+ async function processUser(id: string): Promise<ServiceError | ProcessedUser> {
205
+ const user = await getUser(id) // returns NotFoundError | User
206
+
207
+ if (user instanceof Error) {
208
+ return new ServiceError({ id, cause: user })
209
+ }
210
+
211
+ return process(user)
212
+ }
213
+
214
+ // Access original error via cause
215
+ const result = await processUser('123')
216
+ if (result instanceof Error) {
217
+ console.log(result.message) // "Failed to process user 123"
218
+
219
+ if (result.cause instanceof NotFoundError) {
220
+ console.log(result.cause.id) // access original error's properties
221
+ }
222
+ }
223
+ ```
224
+
225
+ The error definitions:
226
+
227
+ ```ts
228
+ import * as errore from 'errore'
229
+
230
+ class NotFoundError extends errore.createTaggedError({
231
+ name: 'NotFoundError',
232
+ message: 'User $id not found'
233
+ }) {}
234
+
235
+ class ServiceError extends errore.createTaggedError({
236
+ name: 'ServiceError',
237
+ message: 'Failed to process user $id'
238
+ }) {}
239
+ ```
240
+
241
+ **Browser console** prints the full cause chain:
242
+
243
+ ```
244
+ ServiceError: Failed to process user 123
245
+ at processUser (app.js:12)
246
+ at main (app.js:20)
247
+ Caused by: NotFoundError: User 123 not found
248
+ at getUser (app.js:5)
249
+ at processUser (app.js:8)
250
+ ```
251
+
252
+ ### findCause
253
+
254
+ Walk the `.cause` chain to find an ancestor matching a specific error class. Similar to Go's `errors.As` — checks the error itself first, then traverses `.cause` recursively:
255
+
256
+ ```ts
257
+ import * as errore from 'errore'
258
+
259
+ class NotFoundError extends errore.createTaggedError({
260
+ name: 'NotFoundError',
261
+ message: 'User $id not found'
262
+ }) {}
263
+
264
+ class ServiceError extends errore.createTaggedError({
265
+ name: 'ServiceError',
266
+ message: 'Failed to process user $id'
267
+ }) {}
268
+
269
+ // Deep chain: ServiceError -> NotFoundError
270
+ const notFound = new NotFoundError({ id: '123' })
271
+ const service = new ServiceError({ id: '123', cause: notFound })
272
+
273
+ // Instance method on tagged errors
274
+ const found = service.findCause(NotFoundError)
275
+ found?.id // '123' — type-safe access
276
+
277
+ // Standalone function for any Error
278
+ const found2 = errore.findCause(service, NotFoundError)
279
+ found2?.id // '123'
280
+ ```
281
+
282
+ This solves the problem where `result.cause instanceof MyError` only checks one level deep. `findCause` walks the entire chain:
283
+
284
+ ```ts
285
+ // A -> B -> C chain
286
+ const c = new DbError({ message: 'connection reset' })
287
+ const b = new ServiceError({ id: '123', cause: c })
288
+ const a = new ApiError({ message: 'request failed', cause: b })
289
+
290
+ // Manual check only finds B
291
+ a.cause instanceof DbError // false — only checks one level
292
+
293
+ // findCause walks the full chain
294
+ a.findCause(DbError) // finds C ✓
295
+ ```
296
+
297
+ Returns `undefined` if no matching ancestor is found. Safe against circular `.cause` references.
298
+
299
+ ### Custom Base Class with `extends`
300
+
301
+ Use `extends` to inherit from a custom base class. The error will pass `instanceof` for both the base class and the specific error class:
302
+
303
+ ```ts
304
+ import * as errore from 'errore'
305
+
306
+ class AppError extends Error {
307
+ statusCode = 500
308
+ toResponse() { return { error: this.message, code: this.statusCode } }
309
+ }
310
+
311
+ class NotFoundError extends errore.createTaggedError({
312
+ name: 'NotFoundError',
313
+ message: 'Resource $id not found',
314
+ extends: AppError
315
+ }) {
316
+ statusCode = 404
317
+ }
318
+
319
+ const err = new NotFoundError({ id: '123' })
320
+ err instanceof NotFoundError // true
321
+ err instanceof AppError // true
322
+ err instanceof Error // true
323
+
324
+ err.statusCode // 404
325
+ err.toResponse() // { error: 'Resource 123 not found', code: 404 }
326
+ ```
327
+
189
328
  ### Type Guards
190
329
 
330
+ Use **instanceof checks** to narrow union types:
331
+
191
332
  ```ts
192
333
  const result: NetworkError | User = await fetchUser(id)
193
334
 
@@ -200,14 +341,16 @@ if (result instanceof Error) {
200
341
 
201
342
  ### Try Functions
202
343
 
344
+ **Wrap exceptions** as error values:
345
+
203
346
  ```ts
204
347
  import * as errore from 'errore'
205
348
 
206
349
  // Sync - wraps exceptions in UnhandledError
207
- const parsed = errore.tryFn(() => JSON.parse(input))
350
+ const parsed = errore.try(() => JSON.parse(input))
208
351
 
209
352
  // Sync - with custom error type
210
- const parsed = errore.tryFn({
353
+ const parsed = errore.try({
211
354
  try: () => JSON.parse(input),
212
355
  catch: e => new ParseError({ reason: e.message, cause: e })
213
356
  })
@@ -224,6 +367,8 @@ const response = await errore.tryAsync({
224
367
 
225
368
  ### Transformations
226
369
 
370
+ **Transform and chain** operations:
371
+
227
372
  ```ts
228
373
  import * as errore from 'errore'
229
374
 
@@ -242,6 +387,8 @@ const logged = errore.tap(user, u => console.log('Got user:', u.name))
242
387
 
243
388
  ### Extraction
244
389
 
390
+ **Extract values** or throw, **split arrays** by success/error:
391
+
245
392
  ```ts
246
393
  import * as errore from 'errore'
247
394
 
@@ -264,7 +411,7 @@ const [users, errors] = errore.partition(results)
264
411
 
265
412
  ### Error Matching
266
413
 
267
- Always assign `matchError` results to a variable. Keep callbacks pure (return values only) and move side effects outside:
414
+ **Exhaustive pattern matching** with `matchError`. Always assign results to a variable and keep callbacks pure:
268
415
 
269
416
  ```ts
270
417
  import * as errore from 'errore'
@@ -298,7 +445,7 @@ ValidationError.is(value) // specific class
298
445
 
299
446
  ## How Type Safety Works
300
447
 
301
- TypeScript narrows types after `instanceof Error` checks:
448
+ TypeScript **narrows types** after `instanceof Error` checks:
302
449
 
303
450
  ```ts
304
451
  function example(result: NetworkError | User): string {
@@ -318,7 +465,7 @@ This works because:
318
465
 
319
466
  ## Result + Option Combined: `Error | T | null`
320
467
 
321
- One of errore's best features: you can naturally combine error handling with optional values. No wrapper nesting needed!
468
+ Naturally combine **error handling with optional values**. No wrapper nesting needed!
322
469
 
323
470
  ```ts
324
471
  import * as errore from 'errore'
@@ -362,14 +509,80 @@ console.log(user.name)
362
509
  | Zig | `!?T` (error union + optional) | Yes, specific syntax |
363
510
  | **errore** | `Error \| T \| null` | **No!** Check in any order |
364
511
 
365
- With errore:
512
+ With errore you **check in any order**:
366
513
  - Use `?.` and `??` naturally
367
514
  - Check `instanceof Error` or `=== null` in any order
368
515
  - No unwrapping ceremony
369
516
  - TypeScript infers everything
370
517
 
518
+ ## Why This Is Better Than Go
519
+
520
+ Go's error handling uses **two separate return values**:
521
+
522
+ ```go
523
+ user, err := fetchUser(id)
524
+ // Oops! Forgot to check err
525
+ fmt.Println(user.Name) // Compiles fine, crashes at runtime
526
+ ```
527
+
528
+ The compiler can't save you here. You can ignore `err` entirely and use `user` directly.
529
+
530
+ With errore, **forgetting to check is impossible**:
531
+
532
+ ```ts
533
+ const user = await fetchUser(id) // type: NotFoundError | User
534
+
535
+ console.log(user.id) // TS Error: Property 'id' does not exist on type 'NotFoundError'
536
+ ```
537
+
538
+ 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.
539
+
540
+ > **Note:** Properties that exist on both `Error` and your value type (like `name`, `message`) can still be accessed without narrowing. This is a small set of 4 fields: `name`, `message`, `stack`, `cause`.
541
+
542
+ ### The Remaining Gap
543
+
544
+ There's still one case errore can't catch: **ignored return values**:
545
+
546
+ ```ts
547
+ // Oops! Completely ignoring the return value
548
+ updateUser(id, data) // No error, but we should check!
549
+ ```
550
+
551
+ For this, use **TypeScript's built-in checks** or a linter:
552
+
553
+ **TypeScript `tsconfig.json`:**
554
+ ```json
555
+ {
556
+ "compilerOptions": {
557
+ "noUnusedLocals": true
558
+ }
559
+ }
560
+ ```
561
+
562
+ This catches unused variables, though not ignored return values directly.
563
+
564
+ **oxlint `no-unused-expressions`:**
565
+
566
+ `oxlint.json`:
567
+ ```json
568
+ {
569
+ "rules": {
570
+ "no-unused-expressions": "error"
571
+ }
572
+ }
573
+ ```
574
+
575
+ Or via CLI:
576
+ ```bash
577
+ oxlint --deny no-unused-expressions
578
+ ```
579
+
580
+ Combined with errore's type safety, these tools give you near-complete protection against ignored errors.
581
+
371
582
  ## Comparison with Result Types
372
583
 
584
+ **Direct returns** vs wrapper methods:
585
+
373
586
  | Result Pattern | errore |
374
587
  |---------------|--------|
375
588
  | `Result.ok(value)` | just `return value` |
@@ -379,9 +592,150 @@ With errore:
379
592
  | `Result<User, Error>` | `Error \| User` |
380
593
  | `Result<Option<T>, E>` | `Error \| T \| null` |
381
594
 
595
+ ## Vs neverthrow / better-result
596
+
597
+ These libraries wrap values in a **Result container**. You construct results with `ok()` and `err()`, then unwrap them with `.value` and `.error`:
598
+
599
+ ```ts
600
+ // neverthrow
601
+ import { ok, err, Result } from 'neverthrow'
602
+
603
+ function getUser(id: string): Result<User, NotFoundError> {
604
+ const user = db.find(id)
605
+ if (!user) return err(new NotFoundError({ id }))
606
+ return ok(user) // must wrap
607
+ }
608
+
609
+ const result = getUser('123')
610
+ if (result.isErr()) {
611
+ console.log(result.error) // must unwrap
612
+ return
613
+ }
614
+ console.log(result.value.name) // must unwrap
615
+ ```
616
+
617
+ ```ts
618
+ // errore
619
+ function getUser(id: string): User | NotFoundError {
620
+ const user = db.find(id)
621
+ if (!user) return new NotFoundError({ id })
622
+ return user // just return
623
+ }
624
+
625
+ const user = getUser('123')
626
+ if (user instanceof Error) {
627
+ console.log(user) // it's already the error
628
+ return
629
+ }
630
+ console.log(user.name) // it's already the user
631
+ ```
632
+
633
+ **The key insight**: `T | Error` already encodes success/failure. TypeScript's type narrowing does the rest. No wrapper needed.
634
+
635
+ | Feature | neverthrow | errore |
636
+ |---------|------------|--------|
637
+ | Type-safe errors | ✓ | ✓ |
638
+ | Exhaustive handling | ✓ | ✓ |
639
+ | Works with null | `Result<T \| null, E>` | `T \| E \| null` |
640
+ | Learning curve | New API (`ok`, `err`, `map`, `andThen`, ...) | Just `instanceof` |
641
+ | Bundle size | ~3KB min | **~0 bytes** |
642
+ | Interop | Requires wrapping/unwrapping at boundaries | Native TypeScript |
643
+
644
+ 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.
645
+
646
+ ## Vs Effect.ts
647
+
648
+ Effect is not just error handling—it's a **complete functional programming framework** with dependency injection, concurrency primitives, resource management, streaming, and more.
649
+
650
+ ```ts
651
+ // Effect.ts - a paradigm shift
652
+ import { Effect, pipe } from 'effect'
653
+
654
+ const program = pipe(
655
+ fetchUser(id),
656
+ Effect.flatMap(user => fetchPosts(user.id)),
657
+ Effect.map(posts => posts.filter(p => p.published)),
658
+ Effect.catchTag('NotFoundError', () => Effect.succeed([]))
659
+ )
660
+
661
+ const result = await Effect.runPromise(program)
662
+ ```
663
+
664
+ ```ts
665
+ // errore - regular TypeScript
666
+ const user = await fetchUser(id)
667
+ if (user instanceof Error) return []
668
+
669
+ const posts = await fetchPosts(user.id)
670
+ if (posts instanceof Error) return []
671
+
672
+ return posts.filter(p => p.published)
673
+ ```
674
+
675
+ Effect is powerful if you need its full feature set. But if you just want type-safe errors:
676
+
677
+ | | Effect | errore |
678
+ |-|--------|--------|
679
+ | Learning curve | Steep (new paradigm) | Minimal (just `instanceof`) |
680
+ | Codebase impact | Pervasive (everything becomes an Effect) | Surgical (adopt incrementally) |
681
+ | Bundle size | ~50KB+ | **~0 bytes** |
682
+ | Use case | Full FP framework | Just error handling |
683
+
684
+ **Use Effect** when you want dependency injection, structured concurrency, and the full functional programming experience.
685
+
686
+ **Use errore** when you just want type-safe errors without rewriting your codebase.
687
+
688
+ ## Zero-Dependency Philosophy
689
+
690
+ errore is more a **way of writing code** than a library. The core pattern requires nothing:
691
+
692
+ ```ts
693
+ // You can write this without installing errore at all
694
+ class NotFoundError extends Error {
695
+ readonly _tag = 'NotFoundError'
696
+ constructor(public id: string) {
697
+ super(`User ${id} not found`)
698
+ }
699
+ }
700
+
701
+ async function getUser(id: string): Promise<User | NotFoundError> {
702
+ const user = await db.find(id)
703
+ if (!user) return new NotFoundError(id)
704
+ return user
705
+ }
706
+
707
+ const user = await getUser('123')
708
+ if (user instanceof Error) return user
709
+ console.log(user.name)
710
+ ```
711
+
712
+ The `errore` package just provides conveniences: `createTaggedError` for less boilerplate, `matchError` for exhaustive pattern matching, `tryAsync` for catching exceptions. But the core pattern—**errors as union types**—works with zero dependencies.
713
+
714
+ ### Perfect for Libraries
715
+
716
+ Ideal for library authors. Return **plain TypeScript unions** instead of forcing users to adopt your error handling framework:
717
+
718
+ ```ts
719
+ // ❌ Library that forces a dependency on users
720
+ import { Result } from 'some-result-lib'
721
+ export function parse(input: string): Result<AST, ParseError>
722
+
723
+ // Users must now install and learn 'some-result-lib'
724
+ ```
725
+
726
+ ```ts
727
+ // ✓ Library using plain TypeScript unions
728
+ export function parse(input: string): AST | ParseError
729
+
730
+ // Users handle errors with standard instanceof checks
731
+ // No new dependencies, no new concepts to learn
732
+ ```
733
+
734
+ Your library stays lightweight. Users get type-safe errors without adopting an opinionated wrapper. Everyone wins.
735
+
382
736
  ## Import Style
383
737
 
384
- > **Note:** Always use `import * as errore from 'errore'` instead of named imports. This makes code easier to move between files, and more readable for people unfamiliar with errore since every function call is clearly namespaced (e.g. `errore.isOk()` instead of just `isOk()`).
738
+ > **Note:** Always use `import * as errore from 'errore'` instead of named imports. This makes code easier to move between files, and more readable since every function call is **clearly namespaced** (e.g. `errore.isOk()` instead of just `isOk()`).
385
739
 
386
740
  ## License
387
741