errore 0.12.0 → 0.13.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 +25 -8
- package/SKILL.md +128 -74
- package/dist/error.d.ts +30 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +49 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.test.js +104 -1
- package/package.json +1 -1
- package/src/error.ts +48 -0
- package/src/index.test.ts +124 -0
- package/src/index.ts +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,22 @@ console.log(user.username) // user is User, fully narrowed
|
|
|
26
26
|
npm install errore
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
## LLM Skill
|
|
30
|
+
|
|
31
|
+
errore ships with a skill file that teaches AI coding agents (Claude Code, Cursor, Windsurf, etc.) how to use the errore convention correctly. Install it with:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx skills add remorses/errore
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then add this line to your project's `AGENTS.md` or `claude.md`:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
This codebase uses the errore.org convention. ALWAYS read the errore skill before editing any code.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This ensures the agent reads the full errore philosophy and rules before making changes to your codebase.
|
|
44
|
+
|
|
29
45
|
## Quick Start
|
|
30
46
|
|
|
31
47
|
Define typed errors with **variable interpolation** and return **Error or Value** directly:
|
|
@@ -46,10 +62,8 @@ class DbError extends errore.createTaggedError({
|
|
|
46
62
|
|
|
47
63
|
// Function returns Error | Value (no wrapper!)
|
|
48
64
|
async function getUser(id: string): Promise<NotFoundError | DbError | User> {
|
|
49
|
-
const result = await
|
|
50
|
-
|
|
51
|
-
catch: e => new DbError({ reason: e.message, cause: e })
|
|
52
|
-
})
|
|
65
|
+
const result = await db.query(id)
|
|
66
|
+
.catch((e) => new DbError({ reason: e.message, cause: e }))
|
|
53
67
|
|
|
54
68
|
if (result instanceof Error) return result
|
|
55
69
|
if (!result) return new NotFoundError({ id })
|
|
@@ -355,10 +369,11 @@ const parsed = errore.try({
|
|
|
355
369
|
catch: e => new ParseError({ reason: e.message, cause: e })
|
|
356
370
|
})
|
|
357
371
|
|
|
358
|
-
// Async
|
|
359
|
-
const response = await
|
|
372
|
+
// Async — prefer .catch() for promises (no wrapper needed)
|
|
373
|
+
const response = await fetch(url)
|
|
374
|
+
.catch((e) => new NetworkError({ url, cause: e }))
|
|
360
375
|
|
|
361
|
-
// Async
|
|
376
|
+
// Async — errore.tryAsync also works, but .catch() is preferred
|
|
362
377
|
const response = await errore.tryAsync({
|
|
363
378
|
try: () => fetch(url),
|
|
364
379
|
catch: e => new NetworkError({ url, cause: e })
|
|
@@ -366,6 +381,8 @@ const response = await errore.tryAsync({
|
|
|
366
381
|
```
|
|
367
382
|
|
|
368
383
|
> **Best practices for `try` / `tryAsync`:**
|
|
384
|
+
> - **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.
|
|
385
|
+
> - **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.
|
|
369
386
|
> - **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.
|
|
370
387
|
> - **Keep the callback minimal** — wrap only the single throwing call, not your business logic. The `try` callback should be a one-liner.
|
|
371
388
|
> - **Always prefer `errore.try` over `errore.tryFn`** — they are the same function, but `try` is the canonical name.
|
|
@@ -764,7 +781,7 @@ if (user instanceof Error) return user
|
|
|
764
781
|
console.log(user.name)
|
|
765
782
|
```
|
|
766
783
|
|
|
767
|
-
The `errore` package just provides conveniences: `createTaggedError` for less boilerplate, `matchError` for exhaustive pattern matching, `
|
|
784
|
+
The `errore` package just provides conveniences: `createTaggedError` for less boilerplate, `matchError` for exhaustive pattern matching, `try` for catching sync exceptions (and `.catch()` for async promises). But the core pattern—**errors as union types**—works with zero dependencies.
|
|
768
785
|
|
|
769
786
|
### Perfect for Libraries
|
|
770
787
|
|
package/SKILL.md
CHANGED
|
@@ -28,7 +28,7 @@ console.log(user.name) // TypeScript knows: User
|
|
|
28
28
|
1. Always `import * as errore from 'errore'` — namespace import, never destructure
|
|
29
29
|
2. Never throw for expected failures — return errors as values
|
|
30
30
|
3. Never return `unknown | Error` — the union collapses to `unknown`, breaks narrowing
|
|
31
|
-
4. Avoid `try-catch` for control flow — use `
|
|
31
|
+
4. Avoid `try-catch` for control flow — use `.catch()` for async boundaries, `errore.try` for sync boundaries
|
|
32
32
|
5. Use `createTaggedError` for domain errors — gives you `_tag`, typed properties, `$variable` interpolation, `cause`, `findCause`, `toJSON`, and fingerprinting
|
|
33
33
|
6. Let TypeScript infer return types — only add explicit annotations when they improve readability (complex unions, public APIs) or when inference produces a wider type than intended
|
|
34
34
|
7. Use `cause` to wrap errors — `new MyError({ ..., cause: originalError })`
|
|
@@ -36,9 +36,12 @@ console.log(user.name) // TypeScript knows: User
|
|
|
36
36
|
9. Use `const` + expressions, never `let` + try-catch — ternaries, IIFEs, `instanceof Error`
|
|
37
37
|
10. Use early returns, never nested if-else — check error, return, continue flat
|
|
38
38
|
11. Always include `Error` handler in `matchError` — required fallback for plain Error instances
|
|
39
|
-
12. Use `
|
|
40
|
-
13.
|
|
41
|
-
14. Always
|
|
39
|
+
12. Use `.catch()` for async boundaries, `errore.try` for sync boundaries — only at the lowest call stack level where you interact with uncontrolled dependencies (third-party libs, `JSON.parse`, `fetch`, file I/O). Your own code should return errors as values, not throw.
|
|
40
|
+
13. Always wrap `.catch()` in a tagged domain error — `.catch((e) => new MyError({ cause: e }))`. The `.catch()` callback receives `any`, but wrapping in a typed error gives the union a concrete type. Never use `.catch((e) => e as Error)` — always wrap.
|
|
41
|
+
14. Always pass `cause` in `.catch()` callbacks — `.catch((e) => new MyError({ cause: e }))`, never `.catch(() => new MyError())`. Without `cause`, the original error is lost and `isAbortError` can't walk the chain to detect aborts. The `cause` preserves the full error chain for debugging and abort detection.
|
|
42
|
+
15. Always prefer `errore.try` over `errore.tryFn` — they are the same function, but `errore.try` is the canonical name
|
|
43
|
+
16. Use `errore.isAbortError` to detect abort errors — never check `error.name === 'AbortError'` manually, because tagged abort errors have their tag as `.name`
|
|
44
|
+
17. Custom abort errors MUST extend `errore.AbortError` — so `isAbortError` detects them in the cause chain even when wrapped by `.catch()`
|
|
42
45
|
|
|
43
46
|
## TypeScript Rules
|
|
44
47
|
|
|
@@ -91,13 +94,21 @@ These TypeScript practices complement errore's philosophy:
|
|
|
91
94
|
const items = results.filter(isTruthy)
|
|
92
95
|
```
|
|
93
96
|
|
|
94
|
-
- **`controller.abort(
|
|
97
|
+
- **`controller.abort()` must use typed errors** — `abort(reason)` throws `reason` as-is. MUST pass a tagged error extending `errore.AbortError`, NEVER `new Error()` or a string — otherwise `isAbortError` can't detect it in the cause chain:
|
|
95
98
|
```ts
|
|
96
|
-
// BAD:
|
|
99
|
+
// BAD: plain Error — isAbortError won't recognize it
|
|
100
|
+
controller.abort(new Error('timeout'))
|
|
101
|
+
|
|
102
|
+
// BAD: string — not an Error, breaks instanceof checks
|
|
97
103
|
controller.abort('timeout')
|
|
98
104
|
|
|
99
|
-
// GOOD:
|
|
100
|
-
|
|
105
|
+
// GOOD: tagged error extending AbortError
|
|
106
|
+
class TimeoutError extends errore.createTaggedError({
|
|
107
|
+
name: 'TimeoutError',
|
|
108
|
+
message: 'Request timed out for $operation',
|
|
109
|
+
extends: errore.AbortError,
|
|
110
|
+
}) {}
|
|
111
|
+
controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
101
112
|
```
|
|
102
113
|
|
|
103
114
|
- **Never silently suppress errors in catch blocks** — empty `catch {}` hides failures. With errore you rarely need catch at all, but at boundaries where you must, always handle or log:
|
|
@@ -106,10 +117,8 @@ These TypeScript practices complement errore's philosophy:
|
|
|
106
117
|
try { await sendEmail(user.email) } catch {}
|
|
107
118
|
|
|
108
119
|
// GOOD: log and continue if non-critical
|
|
109
|
-
const emailResult = await
|
|
110
|
-
|
|
111
|
-
catch: (e) => new EmailError({ email: user.email, cause: e }),
|
|
112
|
-
})
|
|
120
|
+
const emailResult = await sendEmail(user.email)
|
|
121
|
+
.catch((e) => new EmailError({ email: user.email, cause: e }))
|
|
113
122
|
if (emailResult instanceof Error) {
|
|
114
123
|
console.warn('Failed to send email:', emailResult.message)
|
|
115
124
|
}
|
|
@@ -229,10 +238,8 @@ async function loadConfig(): Promise<Config> {
|
|
|
229
238
|
|
|
230
239
|
// GOOD: flat with errore
|
|
231
240
|
async function loadConfig(): Promise<Config> {
|
|
232
|
-
const raw = await
|
|
233
|
-
|
|
234
|
-
catch: (e) => new ConfigError({ reason: 'Read failed', cause: e }),
|
|
235
|
-
})
|
|
241
|
+
const raw = await fs.readFile('config.json', 'utf-8')
|
|
242
|
+
.catch((e) => new ConfigError({ reason: 'Read failed', cause: e }))
|
|
236
243
|
if (raw instanceof Error) return { port: 3000 }
|
|
237
244
|
|
|
238
245
|
const parsed = errore.try({
|
|
@@ -411,47 +418,43 @@ async function fetchJson(url: string): Promise<any> {
|
|
|
411
418
|
<!-- good -->
|
|
412
419
|
```ts
|
|
413
420
|
async function fetchJson<T>(url: string): Promise<NetworkError | T> {
|
|
414
|
-
const response = await
|
|
415
|
-
|
|
416
|
-
catch: (e) => new NetworkError({ url, reason: 'Fetch failed', cause: e }),
|
|
417
|
-
})
|
|
421
|
+
const response = await fetch(url)
|
|
422
|
+
.catch((e) => new NetworkError({ url, reason: 'Fetch failed', cause: e }))
|
|
418
423
|
if (response instanceof Error) return response
|
|
419
424
|
|
|
420
425
|
if (!response.ok) {
|
|
421
426
|
return new NetworkError({ url, reason: `HTTP ${response.status}` })
|
|
422
427
|
}
|
|
423
428
|
|
|
424
|
-
const data = await
|
|
425
|
-
|
|
426
|
-
catch: (e) => new NetworkError({ url, reason: 'Invalid JSON', cause: e }),
|
|
427
|
-
})
|
|
429
|
+
const data = await (response.json() as Promise<T>)
|
|
430
|
+
.catch((e) => new NetworkError({ url, reason: 'Invalid JSON', cause: e }))
|
|
428
431
|
return data
|
|
429
432
|
}
|
|
430
433
|
```
|
|
431
434
|
|
|
432
|
-
> `
|
|
435
|
+
> `.catch()` on a promise converts rejections to typed errors. TypeScript infers the union (`Response | NetworkError`) automatically. Use `errore.try` for sync boundaries (`JSON.parse`, etc.).
|
|
433
436
|
|
|
434
|
-
###
|
|
437
|
+
### Boundary Rule (.catch for async, errore.try for sync)
|
|
435
438
|
|
|
436
|
-
`
|
|
439
|
+
`.catch()` and `errore.try` should only appear at the **lowest level** of your call stack — right at the boundary with code you don't control (third-party libraries, `JSON.parse`, `fetch`, file I/O, etc.). Your own functions should never throw, so they never need `.catch()` or `try`.
|
|
437
440
|
|
|
438
|
-
|
|
441
|
+
For **async** boundaries: use `.catch((e) => new MyError({ cause: e }))` directly on the promise. TypeScript infers the union automatically.
|
|
439
442
|
|
|
440
|
-
|
|
443
|
+
For **sync** boundaries: use `errore.try({ try: () => ..., catch: (e) => ... })`. Always prefer `errore.try` over `errore.tryFn` — same function, `try` is the canonical name.
|
|
444
|
+
|
|
445
|
+
The `.catch()` callback receives `any` (Promise rejections are untyped), but wrapping in a typed error gives the union a concrete type — no `as` assertions needed.
|
|
441
446
|
|
|
442
447
|
<!-- bad -->
|
|
443
448
|
```ts
|
|
444
|
-
// wrapping too much
|
|
449
|
+
// wrapping too much in a single .catch — business logic should not be here
|
|
445
450
|
async function getUser(id: string): Promise<AppError | User> {
|
|
446
|
-
return
|
|
447
|
-
|
|
448
|
-
const res = await fetch(`/users/${id}`)
|
|
451
|
+
return fetch(`/users/${id}`)
|
|
452
|
+
.then(async (res) => {
|
|
449
453
|
const data = await res.json()
|
|
450
454
|
if (!data.active) throw new Error('inactive')
|
|
451
455
|
return { ...data, displayName: `${data.first} ${data.last}` }
|
|
452
|
-
}
|
|
453
|
-
catch
|
|
454
|
-
})
|
|
456
|
+
})
|
|
457
|
+
.catch((e) => new AppError({ id, cause: e }))
|
|
455
458
|
}
|
|
456
459
|
```
|
|
457
460
|
|
|
@@ -459,30 +462,24 @@ async function getUser(id: string): Promise<AppError | User> {
|
|
|
459
462
|
```ts
|
|
460
463
|
// wrapping your own code that already returns errors as values
|
|
461
464
|
async function processOrder(id: string): Promise<OrderError | Order> {
|
|
462
|
-
return
|
|
463
|
-
|
|
464
|
-
catch: (e) => new OrderError({ id, cause: e }),
|
|
465
|
-
})
|
|
465
|
+
return createOrder(id) // createOrder already returns errors!
|
|
466
|
+
.catch((e) => new OrderError({ id, cause: e }))
|
|
466
467
|
}
|
|
467
468
|
```
|
|
468
469
|
|
|
469
470
|
<!-- good -->
|
|
470
471
|
```ts
|
|
471
|
-
//
|
|
472
|
-
async function getUser(id: string)
|
|
473
|
-
const res = await
|
|
474
|
-
|
|
475
|
-
catch: (e) => new NetworkError({ url: `/users/${id}`, cause: e }),
|
|
476
|
-
})
|
|
472
|
+
// .catch() only wraps the external dependency, nothing else
|
|
473
|
+
async function getUser(id: string) {
|
|
474
|
+
const res = await fetch(`/users/${id}`)
|
|
475
|
+
.catch((e) => new NetworkError({ url: `/users/${id}`, cause: e }))
|
|
477
476
|
if (res instanceof Error) return res
|
|
478
477
|
|
|
479
|
-
const data = await
|
|
480
|
-
|
|
481
|
-
catch: (e) => new NetworkError({ url: `/users/${id}`, cause: e }),
|
|
482
|
-
})
|
|
478
|
+
const data = await (res.json() as Promise<UserPayload>)
|
|
479
|
+
.catch((e) => new NetworkError({ url: `/users/${id}`, cause: e }))
|
|
483
480
|
if (data instanceof Error) return data
|
|
484
481
|
|
|
485
|
-
// business logic is outside
|
|
482
|
+
// business logic is outside .catch — plain code, not wrapped
|
|
486
483
|
if (!data.active) return new InactiveUserError({ id })
|
|
487
484
|
return { ...data, displayName: `${data.first} ${data.last}` }
|
|
488
485
|
}
|
|
@@ -490,15 +487,15 @@ async function getUser(id: string): Promise<NetworkError | User> {
|
|
|
490
487
|
|
|
491
488
|
<!-- good -->
|
|
492
489
|
```ts
|
|
493
|
-
// your own functions return errors as values — no
|
|
494
|
-
async function processOrder(id: string)
|
|
490
|
+
// your own functions return errors as values — no .catch needed
|
|
491
|
+
async function processOrder(id: string) {
|
|
495
492
|
const order = await createOrder(id)
|
|
496
493
|
if (order instanceof Error) return order
|
|
497
494
|
return order
|
|
498
495
|
}
|
|
499
496
|
```
|
|
500
497
|
|
|
501
|
-
> Think of `
|
|
498
|
+
> Think of `.catch()` and `errore.try` as the **adapter** between the throwing world (external code) and the errore world (errors as values). Once you've converted exceptions to values at the boundary, everything above is plain `instanceof` checks.
|
|
502
499
|
|
|
503
500
|
### Optional Values (| null)
|
|
504
501
|
|
|
@@ -514,10 +511,8 @@ async function findUser(email: string): Promise<User | undefined> {
|
|
|
514
511
|
<!-- good -->
|
|
515
512
|
```ts
|
|
516
513
|
async function findUser(email: string): Promise<DbError | User | null> {
|
|
517
|
-
const result = await
|
|
518
|
-
|
|
519
|
-
catch: (e) => new DbError({ message: 'Query failed', cause: e }),
|
|
520
|
-
})
|
|
514
|
+
const result = await db.query(email)
|
|
515
|
+
.catch((e) => new DbError({ message: 'Query failed', cause: e }))
|
|
521
516
|
if (result instanceof Error) return result
|
|
522
517
|
return result ?? null
|
|
523
518
|
}
|
|
@@ -668,17 +663,13 @@ import * as errore from 'errore'
|
|
|
668
663
|
async function processRequest(id: string): Promise<DbError | Result> {
|
|
669
664
|
await using cleanup = new errore.AsyncDisposableStack()
|
|
670
665
|
|
|
671
|
-
const db = await
|
|
672
|
-
|
|
673
|
-
catch: (e) => new DbError({ cause: e }),
|
|
674
|
-
})
|
|
666
|
+
const db = await connectDb()
|
|
667
|
+
.catch((e) => new DbError({ cause: e }))
|
|
675
668
|
if (db instanceof Error) return db
|
|
676
669
|
cleanup.defer(() => db.close())
|
|
677
670
|
|
|
678
|
-
const cache = await
|
|
679
|
-
|
|
680
|
-
catch: (e) => new CacheError({ cause: e }),
|
|
681
|
-
})
|
|
671
|
+
const cache = await openCache()
|
|
672
|
+
.catch((e) => new CacheError({ cause: e }))
|
|
682
673
|
if (cache instanceof Error) return cache
|
|
683
674
|
cleanup.defer(() => cache.flush())
|
|
684
675
|
|
|
@@ -725,10 +716,8 @@ try {
|
|
|
725
716
|
|
|
726
717
|
<!-- good -->
|
|
727
718
|
```ts
|
|
728
|
-
const result = await
|
|
729
|
-
|
|
730
|
-
catch: (e) => new ServiceError({ id, cause: e }),
|
|
731
|
-
})
|
|
719
|
+
const result = await externalService.call(id)
|
|
720
|
+
.catch((e) => new ServiceError({ id, cause: e }))
|
|
732
721
|
if (result instanceof Error) return result
|
|
733
722
|
return result
|
|
734
723
|
```
|
|
@@ -982,6 +971,56 @@ const user = fetchResult instanceof RecordNotFoundError
|
|
|
982
971
|
|
|
983
972
|
> A ternary expression replaces `let` + try-catch + conditional rethrow. One line, no mutation.
|
|
984
973
|
|
|
974
|
+
### Abort & Cancellation
|
|
975
|
+
|
|
976
|
+
`controller.abort(reason)` throws `reason` as-is — whatever you pass is what `.catch()` receives. This means you MUST pass a typed error extending `errore.AbortError`, never a plain `Error` or string.
|
|
977
|
+
|
|
978
|
+
Always use `errore.isAbortError(error)` to detect abort errors. It walks the entire `.cause` chain, so it works even when the abort error is wrapped by `.catch()`.
|
|
979
|
+
|
|
980
|
+
<!-- bad -->
|
|
981
|
+
```ts
|
|
982
|
+
// Plain Error — isAbortError can't detect it
|
|
983
|
+
controller.abort(new Error('timeout'))
|
|
984
|
+
|
|
985
|
+
// String — not an Error, breaks instanceof
|
|
986
|
+
controller.abort('timeout')
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
<!-- good -->
|
|
990
|
+
```ts
|
|
991
|
+
import * as errore from 'errore'
|
|
992
|
+
|
|
993
|
+
class TimeoutError extends errore.createTaggedError({
|
|
994
|
+
name: 'TimeoutError',
|
|
995
|
+
message: 'Request timed out for $operation',
|
|
996
|
+
extends: errore.AbortError,
|
|
997
|
+
}) {}
|
|
998
|
+
|
|
999
|
+
// Pass typed error to abort
|
|
1000
|
+
const controller = new AbortController()
|
|
1001
|
+
const timer = setTimeout(
|
|
1002
|
+
() => controller.abort(new TimeoutError({ operation: 'fetch' })),
|
|
1003
|
+
5000,
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
const res = await fetch(url, { signal: controller.signal })
|
|
1007
|
+
.catch((e) => new NetworkError({ url, cause: e }))
|
|
1008
|
+
clearTimeout(timer)
|
|
1009
|
+
|
|
1010
|
+
if (res instanceof Error) {
|
|
1011
|
+
// Check if the underlying cause was an abort
|
|
1012
|
+
if (errore.isAbortError(res)) {
|
|
1013
|
+
const timeout = errore.findCause(res, TimeoutError)
|
|
1014
|
+
if (timeout) console.log(timeout.operation)
|
|
1015
|
+
return res
|
|
1016
|
+
}
|
|
1017
|
+
// Genuine network error
|
|
1018
|
+
return res
|
|
1019
|
+
}
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
> `isAbortError` detects three kinds of abort: (1) native `DOMException` from bare `controller.abort()`, (2) direct `errore.AbortError` instances, (3) tagged errors that extend `errore.AbortError` — even when wrapped in another error's `.cause` chain.
|
|
1023
|
+
|
|
985
1024
|
## Pitfalls
|
|
986
1025
|
|
|
987
1026
|
### unknown | Error collapses to unknown
|
|
@@ -1067,10 +1106,14 @@ const result = myFn()
|
|
|
1067
1106
|
if (result instanceof Error) return result
|
|
1068
1107
|
// result is string
|
|
1069
1108
|
|
|
1070
|
-
// --- Wrap exceptions ---
|
|
1071
|
-
const data = await
|
|
1072
|
-
|
|
1073
|
-
|
|
1109
|
+
// --- Wrap async exceptions (.catch) ---
|
|
1110
|
+
const data = await riskyAsyncCall()
|
|
1111
|
+
.catch((e) => new MyError({ resource: 'api', reason: 'call failed', cause: e }))
|
|
1112
|
+
|
|
1113
|
+
// --- Wrap sync exceptions (errore.try) ---
|
|
1114
|
+
const parsed = errore.try({
|
|
1115
|
+
try: () => JSON.parse(input),
|
|
1116
|
+
catch: (e) => new MyError({ resource: 'config', reason: 'parse failed', cause: e }),
|
|
1074
1117
|
})
|
|
1075
1118
|
|
|
1076
1119
|
// --- Check errors (plain instanceof, always) ---
|
|
@@ -1092,6 +1135,17 @@ errore.matchError(error, {
|
|
|
1092
1135
|
error.findCause(DbError) // instance method on tagged errors
|
|
1093
1136
|
errore.findCause(error, DbError) // standalone function
|
|
1094
1137
|
|
|
1138
|
+
// --- Abort / Cancellation ---
|
|
1139
|
+
class TimeoutError extends errore.createTaggedError({
|
|
1140
|
+
name: 'TimeoutError',
|
|
1141
|
+
message: 'Request timed out for $operation',
|
|
1142
|
+
extends: errore.AbortError, // MUST extend AbortError
|
|
1143
|
+
}) {}
|
|
1144
|
+
controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
1145
|
+
|
|
1146
|
+
errore.isAbortError(error) // true if abort-related (walks cause chain)
|
|
1147
|
+
errore.findCause(error, TimeoutError) // extract the specific abort error
|
|
1148
|
+
|
|
1095
1149
|
// --- Error properties ---
|
|
1096
1150
|
err._tag // 'MyError'
|
|
1097
1151
|
err.resource // 'user' (from $resource)
|
package/dist/error.d.ts
CHANGED
|
@@ -122,6 +122,36 @@ export declare function matchError<E extends Error, R>(err: E, handlers: MatchHa
|
|
|
122
122
|
* }, (e) => `Unknown: ${e.message}`);
|
|
123
123
|
*/
|
|
124
124
|
export declare function matchErrorPartial<E extends Error, R>(err: E, handlers: Partial<MatchHandlersWithPlain<E, R>>, fallback: (e: E) => R): R;
|
|
125
|
+
/**
|
|
126
|
+
* Base class for abort-related errors.
|
|
127
|
+
* Extend this in custom abort errors so `isAbortError` detects them
|
|
128
|
+
* even when wrapped in a cause chain.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* class TimeoutError extends errore.createTaggedError({
|
|
132
|
+
* name: 'TimeoutError',
|
|
133
|
+
* message: 'Request timed out for $operation',
|
|
134
|
+
* extends: errore.AbortError,
|
|
135
|
+
* }) {}
|
|
136
|
+
*
|
|
137
|
+
* controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
138
|
+
*/
|
|
139
|
+
export declare class AbortError extends Error {
|
|
140
|
+
constructor(message?: string, options?: ErrorOptions);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Check if an error (or any error in its `.cause` chain) is an abort error.
|
|
144
|
+
* Detects native AbortError (DOMException), errore.AbortError, and any
|
|
145
|
+
* tagged error that extends errore.AbortError.
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* const res = await fetch(url, { signal })
|
|
149
|
+
* .catch((e) => new NetworkError({ url, cause: e }))
|
|
150
|
+
* if (errore.isAbortError(res)) {
|
|
151
|
+
* // request was aborted — timeout, user cancel, etc.
|
|
152
|
+
* }
|
|
153
|
+
*/
|
|
154
|
+
export declare function isAbortError(error: unknown): boolean;
|
|
125
155
|
declare const UnhandledError_base: TaggedErrorClass<"UnhandledError", {
|
|
126
156
|
message: string;
|
|
127
157
|
cause: unknown;
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,KAAK,EACvC,KAAK,EAAE,KAAK,EACZ,UAAU,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GACpC,CAAC,GAAG,SAAS,CAUf;AAED;;GAEG;AACH,KAAK,cAAc,GAAG,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AASvD;;GAEG;AACH,KAAK,UAAU,GAAG,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,KAAK,CAAA;AAE/C;;GAEG;AACH,MAAM,MAAM,mBAAmB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,SAAS,KAAK,GAAG,KAAK,IAAI,IAAI,GAAG;IAC9F,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAA;IAClB,+EAA+E;IAC/E,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,GAAG,CAAC,CAAA;IACpC,MAAM,IAAI,MAAM,CAAA;IAChB,iFAAiF;IACjF,SAAS,CAAC,CAAC,SAAS,KAAK,EAAE,UAAU,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,GAAG,SAAS,CAAA;CACjF,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;AAEnB;;GAEG;AACH,MAAM,MAAM,gBAAgB,CAAC,GAAG,SAAS,MAAM,EAAE,KAAK,EAAE,IAAI,SAAS,KAAK,GAAG,KAAK,IAAI;IACpF,KAAK,GAAG,IAAI,EAAE,MAAM,KAAK,SAAS,KAAK,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,GAAG,mBAAmB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;IAC7G,sCAAsC;IACtC,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,mBAAmB,CAAC,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAA;CACnE,CAAA;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,WAAW,EAAE;IACxB,CAAC,GAAG,SAAS,MAAM,EAAE,SAAS,SAAS,UAAU,GAAG,OAAO,KAAK,EAC9D,GAAG,EAAE,GAAG,EACR,SAAS,CAAC,EAAE,SAAS,GACpB,CAAC,KAAK,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,OAAO,gBAAgB,CAAC,GAAG,EAAE,KAAK,EAAE,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IAC1G,8CAA8C;IAC9C,EAAE,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAAA;CAgE5C,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,aAAa,UAhJO,OAAO,KAAG,KAAK,IAAI,cAgJP,CAAA;AAG7C;;GAEG;AACH,KAAK,sBAAsB,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,IAAI;KAC/C,CAAC,IAAI,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE;QAAE,IAAI,EAAE,CAAC,CAAA;KAAE,CAAC,KAAK,CAAC;CAC/E,GAAG;IAAE,KAAK,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,OAAO,CAAC,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;CAAE,CAAA;AAExG;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,QAAQ,EAAE,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAUhG;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,KAAK,EAAE,CAAC,EAClD,GAAG,EAAE,CAAC,EACN,QAAQ,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAC/C,QAAQ,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GACpB,CAAC,CAcH;;aAMU,MAAM;WACR,OAAO;;AALhB;;GAEG;AACH,qBAAa,cAAe,SAAQ,mBAGhC;gBACU,IAAI,EAAE;QAAE,KAAK,EAAE,OAAO,CAAA;KAAE;CAOrC"}
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"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"}
|
package/dist/error.js
CHANGED
|
@@ -178,6 +178,55 @@ export function matchErrorPartial(err, handlers, fallback) {
|
|
|
178
178
|
}
|
|
179
179
|
return fallback(err);
|
|
180
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Base class for abort-related errors.
|
|
183
|
+
* Extend this in custom abort errors so `isAbortError` detects them
|
|
184
|
+
* even when wrapped in a cause chain.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* class TimeoutError extends errore.createTaggedError({
|
|
188
|
+
* name: 'TimeoutError',
|
|
189
|
+
* message: 'Request timed out for $operation',
|
|
190
|
+
* extends: errore.AbortError,
|
|
191
|
+
* }) {}
|
|
192
|
+
*
|
|
193
|
+
* controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
194
|
+
*/
|
|
195
|
+
export class AbortError extends Error {
|
|
196
|
+
constructor(message = 'The operation was aborted', options) {
|
|
197
|
+
super(message, options);
|
|
198
|
+
this.name = 'AbortError';
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if an error (or any error in its `.cause` chain) is an abort error.
|
|
203
|
+
* Detects native AbortError (DOMException), errore.AbortError, and any
|
|
204
|
+
* tagged error that extends errore.AbortError.
|
|
205
|
+
*
|
|
206
|
+
* @example
|
|
207
|
+
* const res = await fetch(url, { signal })
|
|
208
|
+
* .catch((e) => new NetworkError({ url, cause: e }))
|
|
209
|
+
* if (errore.isAbortError(res)) {
|
|
210
|
+
* // request was aborted — timeout, user cancel, etc.
|
|
211
|
+
* }
|
|
212
|
+
*/
|
|
213
|
+
export function isAbortError(error) {
|
|
214
|
+
const seen = new Set();
|
|
215
|
+
let current = error;
|
|
216
|
+
while (current instanceof Error) {
|
|
217
|
+
if (seen.has(current))
|
|
218
|
+
break;
|
|
219
|
+
seen.add(current);
|
|
220
|
+
// Native DOMException AbortError or direct AbortError (name = 'AbortError')
|
|
221
|
+
if (current.name === 'AbortError')
|
|
222
|
+
return true;
|
|
223
|
+
// Tagged errors extending AbortError (createTaggedError overrides name to _tag)
|
|
224
|
+
if (current instanceof AbortError)
|
|
225
|
+
return true;
|
|
226
|
+
current = current.cause;
|
|
227
|
+
}
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
181
230
|
/**
|
|
182
231
|
* Default error type when catching unknown exceptions.
|
|
183
232
|
*/
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export type { Errore, InferError, InferValue, EnsureNotError } from './types.js'
|
|
|
2
2
|
export { isError, isOk, tryFn, tryFn as try, tryAsync } from './core.js';
|
|
3
3
|
export { map, mapError, andThen, andThenAsync, tap, tapAsync } from './transform.js';
|
|
4
4
|
export { unwrap, unwrapOr, match, partition, flatten } from './extract.js';
|
|
5
|
-
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause } from './error.js';
|
|
5
|
+
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause, AbortError, isAbortError } from './error.js';
|
|
6
6
|
export type { TaggedErrorInstance, TaggedErrorClass } from './error.js';
|
|
7
7
|
export { createTaggedError } from './factory.js';
|
|
8
8
|
export type { FactoryTaggedErrorClass, FactoryTaggedErrorInstance } from './factory.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAGxE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGpF,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAG1E,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,YAAY,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAGhF,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,IAAI,GAAG,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAA;AAGxE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAA;AAGpF,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAG1E,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,iBAAiB,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAC3I,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAChD,YAAY,EAAE,uBAAuB,EAAE,0BAA0B,EAAE,MAAM,cAAc,CAAA;AAGvF,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { map, mapError, andThen, andThenAsync, tap, tapAsync } from './transform
|
|
|
5
5
|
// Extraction
|
|
6
6
|
export { unwrap, unwrapOr, match, partition, flatten } from './extract.js';
|
|
7
7
|
// Tagged errors
|
|
8
|
-
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause } from './error.js';
|
|
8
|
+
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause, AbortError, isAbortError } from './error.js';
|
|
9
9
|
// Factory API for tagged errors with $variable interpolation
|
|
10
10
|
export { createTaggedError } from './factory.js';
|
|
11
11
|
// Resource management (DisposableStack polyfills)
|
package/dist/index.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, test, expect } from 'vitest';
|
|
2
|
-
import { isOk, tryFn, tryAsync, map, mapError, andThen, unwrap, unwrapOr, match, partition, TaggedError, matchError, matchErrorPartial, UnhandledError, createTaggedError, findCause, } from './index.js';
|
|
2
|
+
import { isOk, tryFn, tryAsync, map, mapError, andThen, unwrap, unwrapOr, match, partition, TaggedError, matchError, matchErrorPartial, UnhandledError, createTaggedError, findCause, AbortError, isAbortError, } from './index.js';
|
|
3
3
|
// ============================================================================
|
|
4
4
|
// Tagged Error Definitions
|
|
5
5
|
// ============================================================================
|
|
@@ -1139,3 +1139,106 @@ describe('findCause', () => {
|
|
|
1139
1139
|
expect(findCause(b, UnrelatedError)).toBeUndefined();
|
|
1140
1140
|
});
|
|
1141
1141
|
});
|
|
1142
|
+
// ============================================================================
|
|
1143
|
+
// AbortError & isAbortError
|
|
1144
|
+
// ============================================================================
|
|
1145
|
+
describe('AbortError', () => {
|
|
1146
|
+
test('has name AbortError', () => {
|
|
1147
|
+
const err = new AbortError();
|
|
1148
|
+
expect(err.name).toBe('AbortError');
|
|
1149
|
+
expect(err.message).toBe('The operation was aborted');
|
|
1150
|
+
});
|
|
1151
|
+
test('accepts custom message', () => {
|
|
1152
|
+
const err = new AbortError('Request cancelled');
|
|
1153
|
+
expect(err.message).toBe('Request cancelled');
|
|
1154
|
+
expect(err.name).toBe('AbortError');
|
|
1155
|
+
});
|
|
1156
|
+
test('accepts cause via options', () => {
|
|
1157
|
+
const cause = new Error('underlying');
|
|
1158
|
+
const err = new AbortError('aborted', { cause });
|
|
1159
|
+
expect(err.cause).toBe(cause);
|
|
1160
|
+
});
|
|
1161
|
+
test('is instanceof Error', () => {
|
|
1162
|
+
expect(new AbortError()).toBeInstanceOf(Error);
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
describe('isAbortError', () => {
|
|
1166
|
+
test('detects direct AbortError', () => {
|
|
1167
|
+
expect(isAbortError(new AbortError())).toBe(true);
|
|
1168
|
+
});
|
|
1169
|
+
test('detects native DOMException AbortError', () => {
|
|
1170
|
+
const err = new DOMException('aborted', 'AbortError');
|
|
1171
|
+
expect(isAbortError(err)).toBe(true);
|
|
1172
|
+
});
|
|
1173
|
+
test('detects AbortError in cause chain', () => {
|
|
1174
|
+
const abort = new AbortError();
|
|
1175
|
+
const wrapped = new Error('network failed', { cause: abort });
|
|
1176
|
+
expect(isAbortError(wrapped)).toBe(true);
|
|
1177
|
+
});
|
|
1178
|
+
test('detects AbortError deep in cause chain', () => {
|
|
1179
|
+
const abort = new AbortError();
|
|
1180
|
+
const mid = new Error('mid', { cause: abort });
|
|
1181
|
+
const outer = new Error('outer', { cause: mid });
|
|
1182
|
+
expect(isAbortError(outer)).toBe(true);
|
|
1183
|
+
});
|
|
1184
|
+
test('detects tagged error extending AbortError', () => {
|
|
1185
|
+
class TimeoutError extends createTaggedError({
|
|
1186
|
+
name: 'TimeoutError',
|
|
1187
|
+
message: 'Timed out after $duration',
|
|
1188
|
+
extends: AbortError,
|
|
1189
|
+
}) {
|
|
1190
|
+
}
|
|
1191
|
+
const err = new TimeoutError({ duration: '5s' });
|
|
1192
|
+
// createTaggedError overrides name to 'TimeoutError', but instanceof still works
|
|
1193
|
+
expect(err.name).toBe('TimeoutError');
|
|
1194
|
+
expect(err).toBeInstanceOf(AbortError);
|
|
1195
|
+
expect(isAbortError(err)).toBe(true);
|
|
1196
|
+
});
|
|
1197
|
+
test('detects tagged abort error in cause chain (wrapped by .catch)', () => {
|
|
1198
|
+
class TimeoutError extends createTaggedError({
|
|
1199
|
+
name: 'TimeoutError',
|
|
1200
|
+
message: 'Timed out after $duration',
|
|
1201
|
+
extends: AbortError,
|
|
1202
|
+
}) {
|
|
1203
|
+
}
|
|
1204
|
+
class NetworkError extends createTaggedError({
|
|
1205
|
+
name: 'NetworkError',
|
|
1206
|
+
message: 'Request to $url failed',
|
|
1207
|
+
}) {
|
|
1208
|
+
}
|
|
1209
|
+
// Simulates: fetch(url, { signal }).catch((e) => new NetworkError({ url, cause: e }))
|
|
1210
|
+
const timeout = new TimeoutError({ duration: '5s' });
|
|
1211
|
+
const network = new NetworkError({ url: '/api', cause: timeout });
|
|
1212
|
+
expect(isAbortError(network)).toBe(true);
|
|
1213
|
+
// Can also extract the specific error via findCause
|
|
1214
|
+
expect(findCause(network, TimeoutError)).toBe(timeout);
|
|
1215
|
+
});
|
|
1216
|
+
test('returns false for non-abort errors', () => {
|
|
1217
|
+
expect(isAbortError(new Error('oops'))).toBe(false);
|
|
1218
|
+
expect(isAbortError(new TypeError('bad type'))).toBe(false);
|
|
1219
|
+
});
|
|
1220
|
+
test('returns false for non-errors', () => {
|
|
1221
|
+
expect(isAbortError('string')).toBe(false);
|
|
1222
|
+
expect(isAbortError(null)).toBe(false);
|
|
1223
|
+
expect(isAbortError(undefined)).toBe(false);
|
|
1224
|
+
expect(isAbortError(42)).toBe(false);
|
|
1225
|
+
});
|
|
1226
|
+
test('returns false for plain Error passed to abort (not extending AbortError)', () => {
|
|
1227
|
+
// This is why the rule says: MUST use extends: errore.AbortError
|
|
1228
|
+
const err = new Error('timeout');
|
|
1229
|
+
const wrapped = new Error('network failed', { cause: err });
|
|
1230
|
+
expect(isAbortError(wrapped)).toBe(false);
|
|
1231
|
+
});
|
|
1232
|
+
test('detects native DOMException AbortError in cause chain', () => {
|
|
1233
|
+
const abort = new DOMException('aborted', 'AbortError');
|
|
1234
|
+
const wrapped = new Error('fetch failed', { cause: abort });
|
|
1235
|
+
expect(isAbortError(wrapped)).toBe(true);
|
|
1236
|
+
});
|
|
1237
|
+
test('handles circular cause gracefully', () => {
|
|
1238
|
+
const a = new Error('a');
|
|
1239
|
+
const b = new Error('b', { cause: a });
|
|
1240
|
+
a.cause = b;
|
|
1241
|
+
// Should not infinite loop
|
|
1242
|
+
expect(isAbortError(b)).toBe(false);
|
|
1243
|
+
});
|
|
1244
|
+
});
|
package/package.json
CHANGED
package/src/error.ts
CHANGED
|
@@ -256,6 +256,54 @@ export function matchErrorPartial<E extends Error, R>(
|
|
|
256
256
|
return fallback(err)
|
|
257
257
|
}
|
|
258
258
|
|
|
259
|
+
/**
|
|
260
|
+
* Base class for abort-related errors.
|
|
261
|
+
* Extend this in custom abort errors so `isAbortError` detects them
|
|
262
|
+
* even when wrapped in a cause chain.
|
|
263
|
+
*
|
|
264
|
+
* @example
|
|
265
|
+
* class TimeoutError extends errore.createTaggedError({
|
|
266
|
+
* name: 'TimeoutError',
|
|
267
|
+
* message: 'Request timed out for $operation',
|
|
268
|
+
* extends: errore.AbortError,
|
|
269
|
+
* }) {}
|
|
270
|
+
*
|
|
271
|
+
* controller.abort(new TimeoutError({ operation: 'fetch' }))
|
|
272
|
+
*/
|
|
273
|
+
export class AbortError extends Error {
|
|
274
|
+
constructor(message = 'The operation was aborted', options?: ErrorOptions) {
|
|
275
|
+
super(message, options)
|
|
276
|
+
this.name = 'AbortError'
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if an error (or any error in its `.cause` chain) is an abort error.
|
|
282
|
+
* Detects native AbortError (DOMException), errore.AbortError, and any
|
|
283
|
+
* tagged error that extends errore.AbortError.
|
|
284
|
+
*
|
|
285
|
+
* @example
|
|
286
|
+
* const res = await fetch(url, { signal })
|
|
287
|
+
* .catch((e) => new NetworkError({ url, cause: e }))
|
|
288
|
+
* if (errore.isAbortError(res)) {
|
|
289
|
+
* // request was aborted — timeout, user cancel, etc.
|
|
290
|
+
* }
|
|
291
|
+
*/
|
|
292
|
+
export function isAbortError(error: unknown): boolean {
|
|
293
|
+
const seen = new Set<Error>()
|
|
294
|
+
let current: unknown = error
|
|
295
|
+
while (current instanceof Error) {
|
|
296
|
+
if (seen.has(current)) break
|
|
297
|
+
seen.add(current)
|
|
298
|
+
// Native DOMException AbortError or direct AbortError (name = 'AbortError')
|
|
299
|
+
if (current.name === 'AbortError') return true
|
|
300
|
+
// Tagged errors extending AbortError (createTaggedError overrides name to _tag)
|
|
301
|
+
if (current instanceof AbortError) return true
|
|
302
|
+
current = current.cause
|
|
303
|
+
}
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
259
307
|
/**
|
|
260
308
|
* Default error type when catching unknown exceptions.
|
|
261
309
|
*/
|
package/src/index.test.ts
CHANGED
|
@@ -17,6 +17,8 @@ import {
|
|
|
17
17
|
UnhandledError,
|
|
18
18
|
createTaggedError,
|
|
19
19
|
findCause,
|
|
20
|
+
AbortError,
|
|
21
|
+
isAbortError,
|
|
20
22
|
} from './index.js'
|
|
21
23
|
|
|
22
24
|
// ============================================================================
|
|
@@ -608,6 +610,7 @@ describe('real-world: fetch user flow', () => {
|
|
|
608
610
|
err: (e) => `Failed: ${e.message}`,
|
|
609
611
|
})
|
|
610
612
|
|
|
613
|
+
|
|
611
614
|
expect(message).toBe('Failed: Connection failed')
|
|
612
615
|
})
|
|
613
616
|
})
|
|
@@ -1021,6 +1024,7 @@ describe('createTaggedError factory', () => {
|
|
|
1021
1024
|
const originalError = new Error('original')
|
|
1022
1025
|
const err = new WrapperError({ item: 'data', cause: originalError })
|
|
1023
1026
|
|
|
1027
|
+
|
|
1024
1028
|
expect(err.cause).toBe(originalError)
|
|
1025
1029
|
expect(err.message).toBe('Failed to process data')
|
|
1026
1030
|
})
|
|
@@ -1471,3 +1475,123 @@ describe('findCause', () => {
|
|
|
1471
1475
|
expect(findCause(b, UnrelatedError)).toBeUndefined()
|
|
1472
1476
|
})
|
|
1473
1477
|
})
|
|
1478
|
+
|
|
1479
|
+
// ============================================================================
|
|
1480
|
+
// AbortError & isAbortError
|
|
1481
|
+
// ============================================================================
|
|
1482
|
+
|
|
1483
|
+
describe('AbortError', () => {
|
|
1484
|
+
test('has name AbortError', () => {
|
|
1485
|
+
const err = new AbortError()
|
|
1486
|
+
expect(err.name).toBe('AbortError')
|
|
1487
|
+
expect(err.message).toBe('The operation was aborted')
|
|
1488
|
+
})
|
|
1489
|
+
|
|
1490
|
+
test('accepts custom message', () => {
|
|
1491
|
+
const err = new AbortError('Request cancelled')
|
|
1492
|
+
expect(err.message).toBe('Request cancelled')
|
|
1493
|
+
expect(err.name).toBe('AbortError')
|
|
1494
|
+
})
|
|
1495
|
+
|
|
1496
|
+
test('accepts cause via options', () => {
|
|
1497
|
+
const cause = new Error('underlying')
|
|
1498
|
+
const err = new AbortError('aborted', { cause })
|
|
1499
|
+
expect(err.cause).toBe(cause)
|
|
1500
|
+
})
|
|
1501
|
+
|
|
1502
|
+
test('is instanceof Error', () => {
|
|
1503
|
+
expect(new AbortError()).toBeInstanceOf(Error)
|
|
1504
|
+
})
|
|
1505
|
+
})
|
|
1506
|
+
|
|
1507
|
+
describe('isAbortError', () => {
|
|
1508
|
+
test('detects direct AbortError', () => {
|
|
1509
|
+
expect(isAbortError(new AbortError())).toBe(true)
|
|
1510
|
+
})
|
|
1511
|
+
|
|
1512
|
+
test('detects native DOMException AbortError', () => {
|
|
1513
|
+
const err = new DOMException('aborted', 'AbortError')
|
|
1514
|
+
expect(isAbortError(err)).toBe(true)
|
|
1515
|
+
})
|
|
1516
|
+
|
|
1517
|
+
test('detects AbortError in cause chain', () => {
|
|
1518
|
+
const abort = new AbortError()
|
|
1519
|
+
const wrapped = new Error('network failed', { cause: abort })
|
|
1520
|
+
expect(isAbortError(wrapped)).toBe(true)
|
|
1521
|
+
})
|
|
1522
|
+
|
|
1523
|
+
test('detects AbortError deep in cause chain', () => {
|
|
1524
|
+
const abort = new AbortError()
|
|
1525
|
+
const mid = new Error('mid', { cause: abort })
|
|
1526
|
+
const outer = new Error('outer', { cause: mid })
|
|
1527
|
+
expect(isAbortError(outer)).toBe(true)
|
|
1528
|
+
})
|
|
1529
|
+
|
|
1530
|
+
test('detects tagged error extending AbortError', () => {
|
|
1531
|
+
class TimeoutError extends createTaggedError({
|
|
1532
|
+
name: 'TimeoutError',
|
|
1533
|
+
message: 'Timed out after $duration',
|
|
1534
|
+
extends: AbortError,
|
|
1535
|
+
}) {}
|
|
1536
|
+
|
|
1537
|
+
const err = new TimeoutError({ duration: '5s' })
|
|
1538
|
+
// createTaggedError overrides name to 'TimeoutError', but instanceof still works
|
|
1539
|
+
expect(err.name).toBe('TimeoutError')
|
|
1540
|
+
expect(err).toBeInstanceOf(AbortError)
|
|
1541
|
+
expect(isAbortError(err)).toBe(true)
|
|
1542
|
+
})
|
|
1543
|
+
|
|
1544
|
+
test('detects tagged abort error in cause chain (wrapped by .catch)', () => {
|
|
1545
|
+
class TimeoutError extends createTaggedError({
|
|
1546
|
+
name: 'TimeoutError',
|
|
1547
|
+
message: 'Timed out after $duration',
|
|
1548
|
+
extends: AbortError,
|
|
1549
|
+
}) {}
|
|
1550
|
+
|
|
1551
|
+
class NetworkError extends createTaggedError({
|
|
1552
|
+
name: 'NetworkError',
|
|
1553
|
+
message: 'Request to $url failed',
|
|
1554
|
+
}) {}
|
|
1555
|
+
|
|
1556
|
+
// Simulates: fetch(url, { signal }).catch((e) => new NetworkError({ url, cause: e }))
|
|
1557
|
+
const timeout = new TimeoutError({ duration: '5s' })
|
|
1558
|
+
const network = new NetworkError({ url: '/api', cause: timeout })
|
|
1559
|
+
|
|
1560
|
+
expect(isAbortError(network)).toBe(true)
|
|
1561
|
+
// Can also extract the specific error via findCause
|
|
1562
|
+
expect(findCause(network, TimeoutError)).toBe(timeout)
|
|
1563
|
+
})
|
|
1564
|
+
|
|
1565
|
+
test('returns false for non-abort errors', () => {
|
|
1566
|
+
expect(isAbortError(new Error('oops'))).toBe(false)
|
|
1567
|
+
expect(isAbortError(new TypeError('bad type'))).toBe(false)
|
|
1568
|
+
})
|
|
1569
|
+
|
|
1570
|
+
test('returns false for non-errors', () => {
|
|
1571
|
+
expect(isAbortError('string')).toBe(false)
|
|
1572
|
+
expect(isAbortError(null)).toBe(false)
|
|
1573
|
+
expect(isAbortError(undefined)).toBe(false)
|
|
1574
|
+
expect(isAbortError(42)).toBe(false)
|
|
1575
|
+
})
|
|
1576
|
+
|
|
1577
|
+
test('returns false for plain Error passed to abort (not extending AbortError)', () => {
|
|
1578
|
+
// This is why the rule says: MUST use extends: errore.AbortError
|
|
1579
|
+
const err = new Error('timeout')
|
|
1580
|
+
const wrapped = new Error('network failed', { cause: err })
|
|
1581
|
+
expect(isAbortError(wrapped)).toBe(false)
|
|
1582
|
+
})
|
|
1583
|
+
|
|
1584
|
+
test('detects native DOMException AbortError in cause chain', () => {
|
|
1585
|
+
const abort = new DOMException('aborted', 'AbortError')
|
|
1586
|
+
const wrapped = new Error('fetch failed', { cause: abort })
|
|
1587
|
+
expect(isAbortError(wrapped)).toBe(true)
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
test('handles circular cause gracefully', () => {
|
|
1591
|
+
const a = new Error('a')
|
|
1592
|
+
const b = new Error('b', { cause: a })
|
|
1593
|
+
;(a as any).cause = b
|
|
1594
|
+
// Should not infinite loop
|
|
1595
|
+
expect(isAbortError(b)).toBe(false)
|
|
1596
|
+
})
|
|
1597
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { map, mapError, andThen, andThenAsync, tap, tapAsync } from './transform
|
|
|
11
11
|
export { unwrap, unwrapOr, match, partition, flatten } from './extract.js'
|
|
12
12
|
|
|
13
13
|
// Tagged errors
|
|
14
|
-
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause } from './error.js'
|
|
14
|
+
export { TaggedError, matchError, matchErrorPartial, isTaggedError, UnhandledError, findCause, AbortError, isAbortError } from './error.js'
|
|
15
15
|
export type { TaggedErrorInstance, TaggedErrorClass } from './error.js'
|
|
16
16
|
|
|
17
17
|
// Factory API for tagged errors with $variable interpolation
|