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 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 errore.tryAsync({
50
- try: () => db.query(id),
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 errore.tryAsync(() => fetch(url))
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 - with custom error
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, `tryAsync` for catching exceptions. But the core pattern—**errors as union types**—works with zero dependencies.
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 `errore.tryAsync` / `errore.try` to convert exceptions to values
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 `errore.try` / `errore.tryAsync` 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 code should return errors as values, not throw.
40
- 13. Keep the code inside `errore.try` / `errore.tryAsync` minimalwrap only the single throwing call, not your business logic. The `try` callback should be a one-liner calling the external dependency.
41
- 14. Always prefer `errore.try` over `errore.tryFn`they are the same function, but `errore.try` is the canonical name
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(new Error())` not string** — always pass an Error instance to `abort()` so catch blocks receive a real Error with a stack trace, not a string:
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: catch receives a string, not an Error
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: catch receives an Error instance with cause chain
100
- controller.abort(new Error('Request timed out'))
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 errore.tryAsync({
110
- try: () => sendEmail(user.email),
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 errore.tryAsync({
233
- try: () => fs.readFile('config.json', 'utf-8'),
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 errore.tryAsync({
415
- try: () => fetch(url),
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 errore.tryAsync({
425
- try: () => response.json() as Promise<T>,
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
- > `errore.tryAsync` catches exceptions and maps them to typed errors. Use `errore.try` for sync code. The `cause` preserves the original exception.
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
- ### try/tryAsync Placement (Boundary Rule)
437
+ ### Boundary Rule (.catch for async, errore.try for sync)
435
438
 
436
- `errore.try` and `errore.tryAsync` 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 to be wrapped in `try`.
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
- Keep the code inside the `try` callback **as small as possible** ideally a single call to the external dependency. Don't put business logic inside `try`.
441
+ For **async** boundaries: use `.catch((e) => new MyError({ cause: e }))` directly on the promise. TypeScript infers the union automatically.
439
442
 
440
- Always use `errore.try`, never `errore.tryFn` — they are the same function but `errore.try` is the canonical name.
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 code inside try — business logic should not be here
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 errore.tryAsync({
447
- try: async () => {
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: (e) => new AppError({ id, cause: e }),
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 errore.tryAsync({
463
- try: () => createOrder(id), // createOrder already returns errors!
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
- // try only wraps the external dependency (fetch), nothing else
472
- async function getUser(id: string): Promise<NetworkError | User> {
473
- const res = await errore.tryAsync({
474
- try: () => fetch(`/users/${id}`),
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 errore.tryAsync({
480
- try: () => res.json() as Promise<UserPayload>,
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 try — plain code, not wrapped
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 try needed
494
- async function processOrder(id: string): Promise<OrderError | Order> {
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 `errore.try` / `errore.tryAsync` 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.
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 errore.tryAsync({
518
- try: () => db.query(email),
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 errore.tryAsync({
672
- try: () => connectDb(),
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 errore.tryAsync({
679
- try: () => openCache(),
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 errore.tryAsync({
729
- try: () => externalService.call(id),
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 errore.tryAsync({
1072
- try: () => riskyCall(),
1073
- catch: (e) => new MyError({ resource: 'api', reason: 'call failed', cause: e }),
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;
@@ -1 +1 @@
1
- {"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"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"}
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';
@@ -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;AACjH,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"}
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)
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errore",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "type": "module",
5
5
  "description": "Type-safe errors as values for TypeScript. Like Go, but with full type inference.",
6
6
  "repository": {
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