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