go-go-try 7.2.1 → 7.4.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/AGENTS.md +81 -23
- package/README.md +84 -18
- package/dist/index.cjs +104 -61
- package/dist/index.d.cts +217 -94
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +217 -94
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +102 -62
- package/package.json +3 -3
- package/src/assert.ts +59 -0
- package/src/goTry.ts +45 -0
- package/src/goTryAll.ts +141 -0
- package/src/goTryOr.ts +55 -0
- package/src/goTryRaw.ts +126 -0
- package/src/index.test.ts +411 -22
- package/src/index.ts +27 -459
- package/src/internals.ts +45 -0
- package/src/result-helpers.ts +46 -0
- package/src/tagged-error.ts +38 -0
- package/src/types.ts +64 -0
- package/src/unknown-error.ts +20 -0
package/src/index.test.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { attest } from '@ark/attest'
|
|
2
2
|
import { assert, describe, test } from 'vitest'
|
|
3
|
-
|
|
4
|
-
// Helper for exhaustive switch checks - if this function is called,
|
|
5
|
-
// it means we forgot to handle a case in a switch statement
|
|
6
|
-
function assertNever(value: never): never {
|
|
7
|
-
throw new Error(`Unhandled case: ${String(value)}`)
|
|
8
|
-
}
|
|
9
3
|
import {
|
|
10
4
|
type Result,
|
|
11
5
|
type TaggedUnion,
|
|
6
|
+
assert as assertTry,
|
|
7
|
+
assertNever,
|
|
12
8
|
failure,
|
|
13
9
|
goTry,
|
|
14
10
|
goTryAll,
|
|
@@ -19,6 +15,7 @@ import {
|
|
|
19
15
|
isSuccess,
|
|
20
16
|
success,
|
|
21
17
|
taggedError,
|
|
18
|
+
UnknownError,
|
|
22
19
|
} from './index.js'
|
|
23
20
|
|
|
24
21
|
test(`value returned by callback is used when callback doesn't throw`, async () => {
|
|
@@ -323,8 +320,12 @@ describe('edge cases', () => {
|
|
|
323
320
|
assert.equal(value1, undefined)
|
|
324
321
|
assert.equal(value2, undefined)
|
|
325
322
|
assert.equal(err1, 'custom error')
|
|
326
|
-
|
|
327
|
-
assert.equal(
|
|
323
|
+
// goTryRaw now wraps errors in UnknownError by default
|
|
324
|
+
assert.equal(err2 instanceof UnknownError, true)
|
|
325
|
+
assert.equal((err2 as InstanceType<typeof UnknownError>)?._tag, 'UnknownError')
|
|
326
|
+
assert.equal(err2?.message, 'custom error')
|
|
327
|
+
// Original error is preserved in cause
|
|
328
|
+
assert.equal(((err2 as unknown as { cause?: unknown })?.cause as CustomError)?.code, 500)
|
|
328
329
|
})
|
|
329
330
|
|
|
330
331
|
test('throwing a string', () => {
|
|
@@ -362,6 +363,193 @@ describe('edge cases', () => {
|
|
|
362
363
|
})
|
|
363
364
|
})
|
|
364
365
|
|
|
366
|
+
describe('assert helper', () => {
|
|
367
|
+
test('does not throw when condition is true', () => {
|
|
368
|
+
// Should not throw
|
|
369
|
+
assertTry(true, 'should not throw')
|
|
370
|
+
assertTry(1 > 0, new Error('should not throw'))
|
|
371
|
+
assertTry('truthy', 'should not throw')
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
test('throws with string message when condition is false', () => {
|
|
375
|
+
let caught = false
|
|
376
|
+
try {
|
|
377
|
+
assertTry(false, 'custom error message')
|
|
378
|
+
} catch (err) {
|
|
379
|
+
caught = true
|
|
380
|
+
assert.ok(err instanceof Error)
|
|
381
|
+
assert.equal((err as Error).message, 'custom error message')
|
|
382
|
+
}
|
|
383
|
+
assert.equal(caught, true)
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
test('throws with Error instance when condition is false', () => {
|
|
387
|
+
const customError = new Error('custom error instance')
|
|
388
|
+
let caught = false
|
|
389
|
+
try {
|
|
390
|
+
assertTry(false, customError)
|
|
391
|
+
} catch (err) {
|
|
392
|
+
caught = true
|
|
393
|
+
assert.equal(err, customError)
|
|
394
|
+
}
|
|
395
|
+
assert.equal(caught, true)
|
|
396
|
+
})
|
|
397
|
+
|
|
398
|
+
test('throws with tagged error when condition is false', () => {
|
|
399
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
400
|
+
let caught = false
|
|
401
|
+
try {
|
|
402
|
+
assertTry(false, new DatabaseError('database connection failed'))
|
|
403
|
+
} catch (err) {
|
|
404
|
+
caught = true
|
|
405
|
+
assert.ok(err instanceof DatabaseError)
|
|
406
|
+
assert.equal((err as InstanceType<typeof DatabaseError>)._tag, 'DatabaseError')
|
|
407
|
+
assert.equal((err as Error).message, 'database connection failed')
|
|
408
|
+
}
|
|
409
|
+
assert.equal(caught, true)
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
test('type narrowing works with Result types using err === undefined', () => {
|
|
413
|
+
const [err, value] = goTry(() => 'success')
|
|
414
|
+
|
|
415
|
+
// Before assert: err is string | undefined, value is string | undefined
|
|
416
|
+
attest<string | undefined>(err)
|
|
417
|
+
attest<string | undefined>(value)
|
|
418
|
+
|
|
419
|
+
// Using err === undefined provides the best type narrowing
|
|
420
|
+
assertTry(err === undefined, 'should have no error')
|
|
421
|
+
|
|
422
|
+
// TypeScript now knows err is undefined and value is string
|
|
423
|
+
attest<undefined>(err)
|
|
424
|
+
attest<string>(value)
|
|
425
|
+
assert.equal(value, 'success')
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test('type narrowing works with err === undefined check', () => {
|
|
429
|
+
const [err, value] = goTryRaw(() => ({ id: 1, name: 'test' }))
|
|
430
|
+
|
|
431
|
+
// Before assert
|
|
432
|
+
attest<Error | undefined>(err)
|
|
433
|
+
attest<{ id: number; name: string } | undefined>(value)
|
|
434
|
+
|
|
435
|
+
// Using err === undefined pattern
|
|
436
|
+
assertTry(err === undefined, new Error('should have no error'))
|
|
437
|
+
|
|
438
|
+
// After assert: TypeScript knows err is undefined, value is defined
|
|
439
|
+
attest<{ id: number; name: string }>(value)
|
|
440
|
+
assert.deepEqual(value, { id: 1, name: 'test' })
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
test('type narrowing works with tagged errors', () => {
|
|
444
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
445
|
+
const [err, user] = goTryRaw(() => ({ id: '123', name: 'John' }), { errorClass: DatabaseError })
|
|
446
|
+
|
|
447
|
+
// Before assert
|
|
448
|
+
attest<InstanceType<typeof DatabaseError> | undefined>(err)
|
|
449
|
+
attest<{ id: string; name: string } | undefined>(user)
|
|
450
|
+
|
|
451
|
+
// Use assert with tagged error
|
|
452
|
+
assertTry(err === undefined, new DatabaseError('Failed to fetch user'))
|
|
453
|
+
|
|
454
|
+
// After assert: TypeScript knows err is undefined, user is defined
|
|
455
|
+
attest<{ id: string; name: string }>(user)
|
|
456
|
+
assert.deepEqual(user, { id: '123', name: 'John' })
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
test('reduces boilerplate compared to if(err) throw', () => {
|
|
460
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
461
|
+
|
|
462
|
+
function fetchUserOldStyle(): Result<InstanceType<typeof DatabaseError>, { id: string }> {
|
|
463
|
+
const [err, user] = goTryRaw(() => ({ id: '123' }), { errorClass: DatabaseError })
|
|
464
|
+
if (err) return failure(err) // Old style
|
|
465
|
+
return [undefined, user] as const
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function fetchUserNewStyle(): Result<InstanceType<typeof DatabaseError>, { id: string }> {
|
|
469
|
+
const [err, user] = goTryRaw(() => ({ id: '123' }), { errorClass: DatabaseError })
|
|
470
|
+
assertTry(err === undefined, new DatabaseError('Failed to fetch user'))
|
|
471
|
+
// TypeScript now knows user is defined
|
|
472
|
+
return [undefined, user] as const
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const [err1, user1] = fetchUserOldStyle()
|
|
476
|
+
const [err2, user2] = fetchUserNewStyle()
|
|
477
|
+
|
|
478
|
+
assert.equal(err1, undefined)
|
|
479
|
+
assert.equal(err2, undefined)
|
|
480
|
+
assert.deepEqual(user1, { id: '123' })
|
|
481
|
+
assert.deepEqual(user2, { id: '123' })
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('works with falsy values as condition', () => {
|
|
485
|
+
// 0 is falsy - should throw
|
|
486
|
+
assert.throws(() => assertTry(0, 'zero is falsy'))
|
|
487
|
+
|
|
488
|
+
// empty string is falsy - should throw
|
|
489
|
+
assert.throws(() => assertTry('', 'empty string is falsy'))
|
|
490
|
+
|
|
491
|
+
// null is falsy - should throw
|
|
492
|
+
assert.throws(() => assertTry(null, 'null is falsy'))
|
|
493
|
+
|
|
494
|
+
// undefined is falsy - should throw
|
|
495
|
+
assert.throws(() => assertTry(undefined, 'undefined is falsy'))
|
|
496
|
+
|
|
497
|
+
// NaN is falsy - should throw
|
|
498
|
+
assert.throws(() => assertTry(Number.NaN, 'NaN is falsy'))
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('works with truthy values as condition', () => {
|
|
502
|
+
// non-zero number is truthy
|
|
503
|
+
assertTry(1, new Error('one is truthy'))
|
|
504
|
+
|
|
505
|
+
// non-empty string is truthy
|
|
506
|
+
assertTry('hello', new Error('string is truthy'))
|
|
507
|
+
|
|
508
|
+
// object is truthy
|
|
509
|
+
assertTry({}, new Error('object is truthy'))
|
|
510
|
+
|
|
511
|
+
// array is truthy
|
|
512
|
+
assertTry([], new Error('array is truthy'))
|
|
513
|
+
|
|
514
|
+
// true is truthy
|
|
515
|
+
assertTry(true, new Error('true is truthy'))
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test('works with ErrorClass and message (shorter syntax)', () => {
|
|
519
|
+
const ValidationError = taggedError('ValidationError')
|
|
520
|
+
|
|
521
|
+
// Should not throw when condition is true
|
|
522
|
+
assertTry(5 > 0, ValidationError, 'Value must be positive')
|
|
523
|
+
|
|
524
|
+
// Should throw with instantiated error when condition is false
|
|
525
|
+
try {
|
|
526
|
+
assertTry(-1 > 0, ValidationError, 'Value must be positive')
|
|
527
|
+
assert.equal(true, false) // Should not reach here
|
|
528
|
+
} catch (err) {
|
|
529
|
+
assert.ok(err instanceof ValidationError)
|
|
530
|
+
assert.equal((err as InstanceType<typeof ValidationError>)._tag, 'ValidationError')
|
|
531
|
+
assert.equal((err as Error).message, 'Value must be positive')
|
|
532
|
+
}
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('shorter syntax with tagged errors provides type narrowing', () => {
|
|
536
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
537
|
+
const [err, user] = goTryRaw(() => ({ id: '123', name: 'John' }), { errorClass: DatabaseError })
|
|
538
|
+
|
|
539
|
+
// Before assert
|
|
540
|
+
attest<InstanceType<typeof DatabaseError> | undefined>(err)
|
|
541
|
+
attest<{ id: string; name: string } | undefined>(user)
|
|
542
|
+
|
|
543
|
+
// Use assert with shorter syntax
|
|
544
|
+
assertTry(err === undefined, DatabaseError, 'Failed to fetch user')
|
|
545
|
+
|
|
546
|
+
// After assert: TypeScript knows err is undefined, user is defined
|
|
547
|
+
attest<undefined>(err)
|
|
548
|
+
attest<{ id: string; name: string }>(user)
|
|
549
|
+
assert.deepEqual(user, { id: '123', name: 'John' })
|
|
550
|
+
})
|
|
551
|
+
})
|
|
552
|
+
|
|
365
553
|
describe('goTry type tests', () => {
|
|
366
554
|
test('synchronous function returns correct types', () => {
|
|
367
555
|
const fn = () => 'value'
|
|
@@ -1007,8 +1195,8 @@ describe('taggedError', () => {
|
|
|
1007
1195
|
}
|
|
1008
1196
|
|
|
1009
1197
|
// Wrap in functions so goTryRaw can catch the errors
|
|
1010
|
-
const [dbErr, dbResult] = goTryRaw(fetchFromDb, DatabaseError)
|
|
1011
|
-
const [netErr, netResult] = goTryRaw(fetchFromNetwork, NetworkError)
|
|
1198
|
+
const [dbErr, dbResult] = goTryRaw(fetchFromDb, { errorClass: DatabaseError })
|
|
1199
|
+
const [netErr, netResult] = goTryRaw(fetchFromNetwork, { errorClass: NetworkError })
|
|
1012
1200
|
|
|
1013
1201
|
// Type narrowing via discriminated union
|
|
1014
1202
|
if (dbErr) {
|
|
@@ -1034,14 +1222,14 @@ describe('taggedError', () => {
|
|
|
1034
1222
|
async function fetchUser(id: string): Promise<Result<AppError, { id: string; name: string }>> {
|
|
1035
1223
|
const [dbErr, user] = await goTryRaw(
|
|
1036
1224
|
Promise.resolve({ id, name: 'John' }),
|
|
1037
|
-
DatabaseError,
|
|
1225
|
+
{ errorClass: DatabaseError },
|
|
1038
1226
|
)
|
|
1039
1227
|
if (dbErr) return failure<AppError>(dbErr)
|
|
1040
1228
|
return [undefined, user] as const
|
|
1041
1229
|
}
|
|
1042
1230
|
|
|
1043
1231
|
async function fetchData(): Promise<Result<AppError, string>> {
|
|
1044
|
-
const [netErr, data] = await goTryRaw(Promise.resolve('data'), NetworkError)
|
|
1232
|
+
const [netErr, data] = await goTryRaw(Promise.resolve('data'), { errorClass: NetworkError })
|
|
1045
1233
|
if (netErr) return failure<AppError>(netErr)
|
|
1046
1234
|
return [undefined, data] as const
|
|
1047
1235
|
}
|
|
@@ -1079,7 +1267,7 @@ describe('taggedError', () => {
|
|
|
1079
1267
|
case 'ValidationError':
|
|
1080
1268
|
return `VAL: ${err.message}`
|
|
1081
1269
|
default:
|
|
1082
|
-
return
|
|
1270
|
+
return assertNever(err)
|
|
1083
1271
|
}
|
|
1084
1272
|
}
|
|
1085
1273
|
|
|
@@ -1176,7 +1364,7 @@ describe('TaggedUnion type helper', () => {
|
|
|
1176
1364
|
case 'ValidationError':
|
|
1177
1365
|
return `VAL: ${err.message}`
|
|
1178
1366
|
default:
|
|
1179
|
-
return
|
|
1367
|
+
return assertNever(err)
|
|
1180
1368
|
}
|
|
1181
1369
|
}
|
|
1182
1370
|
|
|
@@ -1194,7 +1382,7 @@ describe('TaggedUnion type helper', () => {
|
|
|
1194
1382
|
async function fetchData(): Promise<Result<AppError, string>> {
|
|
1195
1383
|
const [err, data] = await goTryRaw(
|
|
1196
1384
|
Promise.reject(new Error('timeout')),
|
|
1197
|
-
NetworkError,
|
|
1385
|
+
{ errorClass: NetworkError },
|
|
1198
1386
|
)
|
|
1199
1387
|
if (err) return failure<AppError>(err)
|
|
1200
1388
|
return [undefined, data] as const
|
|
@@ -1218,14 +1406,14 @@ describe('inferred return types with tagged errors', () => {
|
|
|
1218
1406
|
// First operation might fail with DatabaseError
|
|
1219
1407
|
const [dbErr, user] = await goTryRaw(
|
|
1220
1408
|
Promise.resolve({ id, name: 'John' }),
|
|
1221
|
-
DatabaseError,
|
|
1409
|
+
{ errorClass: DatabaseError },
|
|
1222
1410
|
)
|
|
1223
1411
|
if (dbErr) return failure(dbErr)
|
|
1224
1412
|
|
|
1225
1413
|
// Second operation might fail with NetworkError
|
|
1226
1414
|
const [netErr, enriched] = await goTryRaw(
|
|
1227
1415
|
Promise.resolve({ ...user!, email: 'john@example.com' }),
|
|
1228
|
-
NetworkError,
|
|
1416
|
+
{ errorClass: NetworkError },
|
|
1229
1417
|
)
|
|
1230
1418
|
if (netErr) return failure(netErr)
|
|
1231
1419
|
|
|
@@ -1263,14 +1451,14 @@ describe('inferred return types with tagged errors', () => {
|
|
|
1263
1451
|
// No explicit return type annotation
|
|
1264
1452
|
function processConfig(input: string) {
|
|
1265
1453
|
// Parse step
|
|
1266
|
-
const [parseErr, parsed] = goTryRaw(() => JSON.parse(input), ParseError)
|
|
1454
|
+
const [parseErr, parsed] = goTryRaw(() => JSON.parse(input), { errorClass: ParseError })
|
|
1267
1455
|
if (parseErr) return failure(parseErr)
|
|
1268
1456
|
|
|
1269
1457
|
// Validate step
|
|
1270
1458
|
const [validateErr, validated] = goTryRaw(() => {
|
|
1271
1459
|
if (!parsed!.port) throw new Error('Missing port')
|
|
1272
1460
|
return parsed as { port: number }
|
|
1273
|
-
}, ValidateError)
|
|
1461
|
+
}, { errorClass: ValidateError })
|
|
1274
1462
|
if (validateErr) return failure(validateErr)
|
|
1275
1463
|
|
|
1276
1464
|
return [undefined, validated] as const
|
|
@@ -1301,19 +1489,19 @@ describe('inferred return types with tagged errors', () => {
|
|
|
1301
1489
|
async function complexOperation(shouldFail: 'a' | 'b' | 'c' | 'none') {
|
|
1302
1490
|
const [errA, valA] = await goTryRaw(
|
|
1303
1491
|
shouldFail === 'a' ? Promise.reject(new Error('a')) : Promise.resolve('step1'),
|
|
1304
|
-
ErrorA,
|
|
1492
|
+
{ errorClass: ErrorA },
|
|
1305
1493
|
)
|
|
1306
1494
|
if (errA) return failure(errA)
|
|
1307
1495
|
|
|
1308
1496
|
const [errB, valB] = await goTryRaw(
|
|
1309
1497
|
shouldFail === 'b' ? Promise.reject(new Error('b')) : Promise.resolve('step2'),
|
|
1310
|
-
ErrorB,
|
|
1498
|
+
{ errorClass: ErrorB },
|
|
1311
1499
|
)
|
|
1312
1500
|
if (errB) return failure(errB)
|
|
1313
1501
|
|
|
1314
1502
|
const [errC, valC] = await goTryRaw(
|
|
1315
1503
|
shouldFail === 'c' ? Promise.reject(new Error('c')) : Promise.resolve('step3'),
|
|
1316
|
-
ErrorC,
|
|
1504
|
+
{ errorClass: ErrorC },
|
|
1317
1505
|
)
|
|
1318
1506
|
if (errC) return failure(errC)
|
|
1319
1507
|
|
|
@@ -1350,3 +1538,204 @@ describe('inferred return types with tagged errors', () => {
|
|
|
1350
1538
|
)
|
|
1351
1539
|
})
|
|
1352
1540
|
})
|
|
1541
|
+
|
|
1542
|
+
|
|
1543
|
+
describe('UnknownError', () => {
|
|
1544
|
+
test('UnknownError is exported as a tagged error class', () => {
|
|
1545
|
+
const err = new UnknownError('something went wrong')
|
|
1546
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1547
|
+
assert.equal(err.message, 'something went wrong')
|
|
1548
|
+
assert.equal(err.name, 'UnknownError')
|
|
1549
|
+
assert.ok(err instanceof Error)
|
|
1550
|
+
assert.ok(err instanceof UnknownError)
|
|
1551
|
+
})
|
|
1552
|
+
|
|
1553
|
+
test('UnknownError supports cause option', () => {
|
|
1554
|
+
const cause = new Error('original error')
|
|
1555
|
+
const err = new UnknownError('wrapped error', { cause })
|
|
1556
|
+
assert.equal(err.cause, cause)
|
|
1557
|
+
})
|
|
1558
|
+
|
|
1559
|
+
test('goTryRaw defaults to UnknownError for system errors', () => {
|
|
1560
|
+
const fn = () => {
|
|
1561
|
+
throw new Error('system error')
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const [err, value] = goTryRaw(fn)
|
|
1565
|
+
|
|
1566
|
+
assert.equal(value, undefined)
|
|
1567
|
+
assert.ok(err instanceof UnknownError)
|
|
1568
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1569
|
+
assert.equal(err.message, 'system error')
|
|
1570
|
+
})
|
|
1571
|
+
|
|
1572
|
+
test('goTryRaw defaults to UnknownError for thrown strings', () => {
|
|
1573
|
+
const fn = () => {
|
|
1574
|
+
throw 'string error'
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const [err, value] = goTryRaw(fn)
|
|
1578
|
+
|
|
1579
|
+
assert.equal(value, undefined)
|
|
1580
|
+
assert.ok(err instanceof UnknownError)
|
|
1581
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1582
|
+
assert.equal(err.message, 'string error')
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
test('goTryRaw defaults to UnknownError for thrown undefined', () => {
|
|
1586
|
+
const fn = () => {
|
|
1587
|
+
throw undefined
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
const [err, value] = goTryRaw(fn)
|
|
1591
|
+
|
|
1592
|
+
assert.equal(value, undefined)
|
|
1593
|
+
assert.ok(err instanceof UnknownError)
|
|
1594
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1595
|
+
assert.equal(err.message, 'undefined')
|
|
1596
|
+
})
|
|
1597
|
+
|
|
1598
|
+
test('goTryRaw with async defaults to UnknownError', async () => {
|
|
1599
|
+
const promise = Promise.reject(new Error('async error'))
|
|
1600
|
+
|
|
1601
|
+
const [err, value] = await goTryRaw(promise)
|
|
1602
|
+
|
|
1603
|
+
assert.equal(value, undefined)
|
|
1604
|
+
assert.ok(err instanceof UnknownError)
|
|
1605
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1606
|
+
assert.equal(err.message, 'async error')
|
|
1607
|
+
})
|
|
1608
|
+
})
|
|
1609
|
+
|
|
1610
|
+
describe('goTryRaw with options object', () => {
|
|
1611
|
+
test('errorClass wraps all errors including tagged ones', () => {
|
|
1612
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1613
|
+
const NetworkError = taggedError('NetworkError')
|
|
1614
|
+
|
|
1615
|
+
// When a DatabaseError is thrown, it gets wrapped in NetworkError
|
|
1616
|
+
const fn = () => {
|
|
1617
|
+
throw new DatabaseError('db connection failed')
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const [err, value] = goTryRaw(fn, { errorClass: NetworkError })
|
|
1621
|
+
|
|
1622
|
+
assert.equal(value, undefined)
|
|
1623
|
+
assert.ok(err instanceof NetworkError)
|
|
1624
|
+
assert.equal(err._tag, 'NetworkError')
|
|
1625
|
+
assert.equal(err.message, 'db connection failed')
|
|
1626
|
+
// Original error is preserved in cause
|
|
1627
|
+
assert.ok(err.cause instanceof DatabaseError)
|
|
1628
|
+
})
|
|
1629
|
+
|
|
1630
|
+
test('errorClass wraps non-tagged errors', () => {
|
|
1631
|
+
const NetworkError = taggedError('NetworkError')
|
|
1632
|
+
|
|
1633
|
+
const fn = () => {
|
|
1634
|
+
throw new Error('plain error')
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
const [err, value] = goTryRaw(fn, { errorClass: NetworkError })
|
|
1638
|
+
|
|
1639
|
+
assert.equal(value, undefined)
|
|
1640
|
+
assert.ok(err instanceof NetworkError)
|
|
1641
|
+
assert.equal(err._tag, 'NetworkError')
|
|
1642
|
+
assert.equal(err.message, 'plain error')
|
|
1643
|
+
})
|
|
1644
|
+
|
|
1645
|
+
test('systemErrorClass only wraps non-tagged errors', () => {
|
|
1646
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1647
|
+
const SystemError = taggedError('SystemError')
|
|
1648
|
+
|
|
1649
|
+
// Tagged errors should pass through
|
|
1650
|
+
const fnTagged = () => {
|
|
1651
|
+
throw new DatabaseError('db error')
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
const [err1, value1] = goTryRaw(fnTagged, { systemErrorClass: SystemError })
|
|
1655
|
+
|
|
1656
|
+
assert.equal(value1, undefined)
|
|
1657
|
+
assert.ok(err1 instanceof DatabaseError)
|
|
1658
|
+
assert.equal(err1._tag, 'DatabaseError')
|
|
1659
|
+
|
|
1660
|
+
// Non-tagged errors should be wrapped
|
|
1661
|
+
const fnPlain = () => {
|
|
1662
|
+
throw new Error('system error')
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const [err2, value2] = goTryRaw(fnPlain, { systemErrorClass: SystemError })
|
|
1666
|
+
|
|
1667
|
+
assert.equal(value2, undefined)
|
|
1668
|
+
assert.ok(err2 instanceof SystemError)
|
|
1669
|
+
assert.equal(err2._tag, 'SystemError')
|
|
1670
|
+
assert.equal(err2.message, 'system error')
|
|
1671
|
+
})
|
|
1672
|
+
|
|
1673
|
+
test('systemErrorClass defaults to UnknownError when not specified', () => {
|
|
1674
|
+
const fn = () => {
|
|
1675
|
+
throw new Error('plain error')
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
const [err, value] = goTryRaw(fn, {})
|
|
1679
|
+
|
|
1680
|
+
assert.equal(value, undefined)
|
|
1681
|
+
assert.ok(err instanceof UnknownError)
|
|
1682
|
+
assert.equal(err._tag, 'UnknownError')
|
|
1683
|
+
})
|
|
1684
|
+
|
|
1685
|
+
test('async with options object', async () => {
|
|
1686
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1687
|
+
|
|
1688
|
+
const promise = Promise.reject(new Error('async error'))
|
|
1689
|
+
|
|
1690
|
+
const [err, value] = await goTryRaw(promise, { errorClass: DatabaseError })
|
|
1691
|
+
|
|
1692
|
+
assert.equal(value, undefined)
|
|
1693
|
+
assert.ok(err instanceof DatabaseError)
|
|
1694
|
+
assert.equal(err._tag, 'DatabaseError')
|
|
1695
|
+
})
|
|
1696
|
+
|
|
1697
|
+
})
|
|
1698
|
+
|
|
1699
|
+
describe('goTryRaw options type tests', () => {
|
|
1700
|
+
test('systemErrorClass preserves tagged errors', () => {
|
|
1701
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1702
|
+
const SystemError = taggedError('SystemError')
|
|
1703
|
+
|
|
1704
|
+
// Wrap in a function so goTryRaw can catch the error
|
|
1705
|
+
const [err, _value] = goTryRaw(() => {
|
|
1706
|
+
throw new DatabaseError('db error')
|
|
1707
|
+
}, { systemErrorClass: SystemError })
|
|
1708
|
+
|
|
1709
|
+
// Type is systemErrorClass since TypeScript cannot know which tagged errors
|
|
1710
|
+
// might be thrown at runtime (tagged errors pass through, others get wrapped)
|
|
1711
|
+
attest<InstanceType<typeof SystemError> | undefined>(err)
|
|
1712
|
+
|
|
1713
|
+
// But at runtime, tagged errors are preserved
|
|
1714
|
+
if (err) {
|
|
1715
|
+
assert.equal(err._tag, 'DatabaseError')
|
|
1716
|
+
}
|
|
1717
|
+
})
|
|
1718
|
+
|
|
1719
|
+
test('errorClass wraps all errors to specified type', () => {
|
|
1720
|
+
const DatabaseError = taggedError('DatabaseError')
|
|
1721
|
+
|
|
1722
|
+
const [err, value] = goTryRaw(() => 'test', { errorClass: DatabaseError })
|
|
1723
|
+
|
|
1724
|
+
attest<InstanceType<typeof DatabaseError> | undefined>(err)
|
|
1725
|
+
attest<string | undefined>(value)
|
|
1726
|
+
})
|
|
1727
|
+
|
|
1728
|
+
test('no options defaults to Error type (backward compatible)', () => {
|
|
1729
|
+
const [err, value] = goTryRaw(() => 'test')
|
|
1730
|
+
|
|
1731
|
+
attest<Error | undefined>(err)
|
|
1732
|
+
attest<string | undefined>(value)
|
|
1733
|
+
})
|
|
1734
|
+
|
|
1735
|
+
test('empty options object defaults to UnknownError type', () => {
|
|
1736
|
+
const [err, value] = goTryRaw(() => 'test', {})
|
|
1737
|
+
|
|
1738
|
+
attest<InstanceType<typeof UnknownError> | undefined>(err)
|
|
1739
|
+
attest<string | undefined>(value)
|
|
1740
|
+
})
|
|
1741
|
+
})
|