@@ -2464,7 +2464,8 @@ describe('createSafe', () => {
24642464 } )
24652465
24662466 describe ( 'parseResult error handling' , ( ) => {
2467- it ( 'factory parseResult throw is caught by parseError' , ( ) => {
2467+ it ( 'factory parseResult throw returns raw result and reports via onHookError' , ( ) => {
2468+ const onHookError = vi . fn ( )
24682469 const appSafe = createSafe ( {
24692470 parseError : ( e ) => ( {
24702471 code : 'PARSE_ERROR' ,
@@ -2473,20 +2474,24 @@ describe('createSafe', () => {
24732474 parseResult : ( ) => {
24742475 throw new Error ( 'validation failed' )
24752476 } ,
2477+ onHookError,
24762478 } )
24772479
24782480 const [ result , error ] = appSafe . sync ( ( ) => 42 )
24792481
2480- expect ( result ) . toBeNull ( )
2481- expect ( error ) . toEqual ( { code : 'PARSE_ERROR' , message : 'validation failed' } )
2482+ expect ( result ) . toBe ( 42 )
2483+ expect ( error ) . toBeNull ( )
2484+ expect ( onHookError ) . toHaveBeenCalledWith ( expect . any ( Error ) , 'parseResult' )
24822485 } )
24832486
2484- it ( 'per-call parseResult throw is caught by factory parseError' , ( ) => {
2487+ it ( 'per-call parseResult throw returns raw result and reports via onHookError' , ( ) => {
2488+ const onHookError = vi . fn ( )
24852489 const appSafe = createSafe ( {
24862490 parseError : ( e ) => ( {
24872491 code : 'PARSE_ERROR' ,
24882492 message : e instanceof Error ? e . message : 'Unknown' ,
24892493 } ) ,
2494+ onHookError,
24902495 } )
24912496
24922497 const [ result , error ] = appSafe . sync ( ( ) => 42 , {
@@ -2495,30 +2500,29 @@ describe('createSafe', () => {
24952500 } ,
24962501 } )
24972502
2498- expect ( result ) . toBeNull ( )
2499- expect ( error ) . toEqual ( { code : 'PARSE_ERROR' , message : 'per-call validation failed' } )
2503+ expect ( result ) . toBe ( 42 )
2504+ expect ( error ) . toBeNull ( )
2505+ expect ( onHookError ) . toHaveBeenCalledWith ( expect . any ( Error ) , 'parseResult' )
25002506 } )
25012507
2502- it ( 'factory parseResult throw triggers retry on async' , async ( ) => {
2503- let attempts = 0
2504- const onRetry = vi . fn ( )
2508+ it ( 'factory parseResult throw does not trigger retry on async' , async ( ) => {
2509+ const fn = vi . fn ( ) . mockResolvedValue ( 'data' )
2510+ const onHookError = vi . fn ( )
25052511
25062512 const appSafe = createSafe ( {
25072513 parseError : ( e ) => String ( e ) ,
2508- parseResult : ( response ) => {
2509- attempts ++
2510- if ( attempts < 3 ) throw new Error ( 'not ready' )
2511- return response
2512- } ,
2514+ parseResult : ( ) => { throw new Error ( 'not ready' ) } ,
25132515 retry : { times : 3 } ,
2514- onRetry ,
2516+ onHookError ,
25152517 } )
25162518
2517- const [ result , error ] = await appSafe . async ( ( ) => Promise . resolve ( 'data' ) )
2519+ const [ result , error ] = await appSafe . async ( fn )
25182520
2519- expect ( error ) . toBeNull ( )
2521+ // parseResult failure is isolated — fn is not retried
2522+ expect ( fn ) . toHaveBeenCalledTimes ( 1 )
25202523 expect ( result ) . toBe ( 'data' )
2521- expect ( onRetry ) . toHaveBeenCalledTimes ( 2 )
2524+ expect ( error ) . toBeNull ( )
2525+ expect ( onHookError ) . toHaveBeenCalledWith ( expect . any ( Error ) , 'parseResult' )
25222526 } )
25232527
25242528 it ( 'parseResult not called on error path' , ( ) => {
@@ -2787,23 +2791,26 @@ describe('createSafe', () => {
27872791 expect ( data ) . toEqual ( { only : 'solo' } )
27882792 } )
27892793
2790- it ( 'parseResult throwing is caught by parseError in all()' , async ( ) => {
2794+ it ( 'parseResult throwing returns raw values in all()' , async ( ) => {
2795+ const onHookError = vi . fn ( )
27912796 const appSafe = createSafe ( {
27922797 parseError : ( e ) => ( {
27932798 code : 'PARSE_RESULT_ERROR' as const ,
27942799 message : e instanceof Error ? e . message : 'unknown' ,
27952800 } ) ,
27962801 parseResult : ( ) => { throw new Error ( 'parseResult exploded' ) } ,
27972802 defaultError : { code : 'PARSE_RESULT_ERROR' as const , message : 'default' } ,
2803+ onHookError,
27982804 } )
27992805
28002806 const [ data , error ] = await appSafe . all ( {
28012807 a : ( ) => Promise . resolve ( 1 ) ,
28022808 b : ( ) => Promise . resolve ( 2 ) ,
28032809 } )
28042810
2805- expect ( data ) . toBeNull ( )
2806- expect ( error ) . toEqual ( { code : 'PARSE_RESULT_ERROR' , message : 'parseResult exploded' } )
2811+ expect ( error ) . toBeNull ( )
2812+ expect ( data ) . toEqual ( { a : 1 , b : 2 } )
2813+ expect ( onHookError ) . toHaveBeenCalledWith ( expect . any ( Error ) , 'parseResult' )
28072814 } )
28082815
28092816 it ( 'handles a function that throws synchronously (before returning a promise)' , async ( ) => {
@@ -2822,6 +2829,47 @@ describe('createSafe', () => {
28222829 expect ( data ) . toBeNull ( )
28232830 expect ( error ) . toEqual ( { code : 'SYNC_THROW' , message : 'sync kaboom' } )
28242831 } )
2832+
2833+ it ( 'catches a raw promise rejection via defensive handler and applies parseError' , async ( ) => {
2834+ // An invalid abortAfter causes validateAbortAfter to throw *before*
2835+ // safeAsync's try-catch, making the returned promise reject outright.
2836+ // The rejection handler on .then() in createSafe.all must catch this
2837+ // and apply parseError (not just toError).
2838+ const appSafe = createSafe ( {
2839+ parseError : ( e ) => ( {
2840+ code : 'ERR' as const ,
2841+ message : e instanceof Error ? e . message : 'unknown' ,
2842+ } ) ,
2843+ abortAfter : - 1 ,
2844+ } )
2845+
2846+ const [ data , error ] = await appSafe . all ( {
2847+ a : ( ) => Promise . resolve ( 1 ) ,
2848+ } )
2849+
2850+ expect ( data ) . toBeNull ( )
2851+ expect ( error ) . toEqual ( { code : 'ERR' , message : expect . stringMatching ( / a b o r t A f t e r / ) } )
2852+ } )
2853+
2854+ it ( 'ignores late rejections after already resolved (rejection handler done guard)' , async ( ) => {
2855+ // Both promises reject via invalid abortAfter. The first rejection
2856+ // resolves the outer promise; the second hits `if (done) return`.
2857+ const appSafe = createSafe ( {
2858+ parseError : ( e ) => ( {
2859+ code : 'ERR' as const ,
2860+ message : e instanceof Error ? e . message : 'unknown' ,
2861+ } ) ,
2862+ abortAfter : - 1 ,
2863+ } )
2864+
2865+ const [ data , error ] = await appSafe . all ( {
2866+ a : ( ) => Promise . resolve ( 1 ) ,
2867+ b : ( ) => Promise . resolve ( 2 ) ,
2868+ } )
2869+
2870+ expect ( data ) . toBeNull ( )
2871+ expect ( error ) . toEqual ( { code : 'ERR' , message : expect . stringMatching ( / a b o r t A f t e r / ) } )
2872+ } )
28252873 } )
28262874
28272875 describe ( 'allSettled' , ( ) => {
@@ -2914,25 +2962,28 @@ describe('createSafe', () => {
29142962 expect ( results . only . value ) . toBe ( 'solo' )
29152963 } )
29162964
2917- it ( 'parseResult throwing is caught by parseError in allSettled()' , async ( ) => {
2965+ it ( 'parseResult throwing returns raw values in allSettled()' , async ( ) => {
2966+ const onHookError = vi . fn ( )
29182967 const appSafe = createSafe ( {
29192968 parseError : ( e ) => ( {
29202969 code : 'PARSE_RESULT_ERROR' as const ,
29212970 message : e instanceof Error ? e . message : 'unknown' ,
29222971 } ) ,
29232972 parseResult : ( ) => { throw new Error ( 'parseResult exploded' ) } ,
29242973 defaultError : { code : 'PARSE_RESULT_ERROR' as const , message : 'default' } ,
2974+ onHookError,
29252975 } )
29262976
29272977 const results = await appSafe . allSettled ( {
29282978 a : ( ) => Promise . resolve ( 1 ) ,
29292979 b : ( ) => Promise . resolve ( 2 ) ,
29302980 } )
29312981
2932- expect ( results . a . ok ) . toBe ( false )
2933- expect ( results . a . error ) . toEqual ( { code : 'PARSE_RESULT_ERROR' , message : 'parseResult exploded' } )
2934- expect ( results . b . ok ) . toBe ( false )
2935- expect ( results . b . error ) . toEqual ( { code : 'PARSE_RESULT_ERROR' , message : 'parseResult exploded' } )
2982+ expect ( results . a . ok ) . toBe ( true )
2983+ expect ( results . a . value ) . toBe ( 1 )
2984+ expect ( results . b . ok ) . toBe ( true )
2985+ expect ( results . b . value ) . toBe ( 2 )
2986+ expect ( onHookError ) . toHaveBeenCalledWith ( expect . any ( Error ) , 'parseResult' )
29362987 } )
29372988 } )
29382989
@@ -3229,4 +3280,41 @@ describe('createSafe', () => {
32293280 } )
32303281 } )
32313282 } )
3283+
3284+ describe ( 'all with empty object' , ( ) => {
3285+ it ( 'returns ok with empty object for empty input' , async ( ) => {
3286+ const appSafe = createSafe ( {
3287+ parseError : ( e ) => ( e instanceof Error ? e . message : 'unknown' ) ,
3288+ } )
3289+
3290+ const [ data , error ] = await appSafe . all ( { } )
3291+
3292+ expect ( error ) . toBeNull ( )
3293+ expect ( data ) . toEqual ( { } )
3294+ } )
3295+
3296+ it ( 'result has ok: true for empty input' , async ( ) => {
3297+ const appSafe = createSafe ( {
3298+ parseError : ( e ) => String ( e ) ,
3299+ } )
3300+
3301+ const result = await appSafe . all ( { } )
3302+
3303+ expect ( result . ok ) . toBe ( true )
3304+ expect ( result . value ) . toEqual ( { } )
3305+ expect ( result . error ) . toBeNull ( )
3306+ } )
3307+ } )
3308+
3309+ describe ( 'allSettled with empty object' , ( ) => {
3310+ it ( 'returns empty object for empty input' , async ( ) => {
3311+ const appSafe = createSafe ( {
3312+ parseError : ( e ) => ( e instanceof Error ? e . message : 'unknown' ) ,
3313+ } )
3314+
3315+ const results = await appSafe . allSettled ( { } )
3316+
3317+ expect ( results ) . toEqual ( { } )
3318+ } )
3319+ } )
32323320} )
0 commit comments