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/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
- assert.equal(err2 instanceof CustomError, true)
327
- assert.equal((err2 as CustomError).code, 500)
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 `UNK: ${err.message}`
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 `UNK: ${err.message}`
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
+ })