Skip to content

Commit 166ffe0

Browse files
committed
fix(loaders): avoids Uncaught PromiseRejection if the signal is thrown
Fix #768
1 parent c9415a2 commit 166ffe0

File tree

2 files changed

+84
-5
lines changed

2 files changed

+84
-5
lines changed

src/data-loaders/navigation-guard.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export function setupLoaderGuard({
152152
}
153153

154154
const loaders: UseDataLoader[] = Array.from(to.meta[LOADER_SET_KEY]!)
155+
const { signal } = to.meta[ABORT_CONTROLLER_KEY]!
155156

156157
// unset the context so all loaders are executed as root loaders
157158
setCurrentContext([])
@@ -217,11 +218,16 @@ export function setupLoaderGuard({
217218
})
218219
.catch((error) =>
219220
error instanceof NavigationResult
220-
? error.value
221-
: // let the error propagate to router.onError()
222-
// we use never because the rejection means we never resolve a value and using anything else
223-
// will not be valid from the navigation guard's perspective
224-
Promise.reject<never>(error)
221+
? // TODO: why? add comment explaining
222+
error.value
223+
: // we don't want to propagate an error if it was our own abort signal
224+
// this includes cancelled navigations + signal.throwIfAborted() calls
225+
signal.aborted && error === signal.reason
226+
? false
227+
: // let the error propagate to router.onError()
228+
// we use never because the rejection means we never resolve a value and using anything else
229+
// will not be valid from the navigation guard's perspective
230+
Promise.reject<never>(error)
225231
)
226232
.finally(() => {
227233
// unset the context so mounting happens without an active context

tests/data-loaders/tester.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import { dataOneSpy, dataTwoSpy } from '../data-loaders/loaders'
3939
import {
4040
createMemoryHistory,
4141
createRouter,
42+
isNavigationFailure,
4243
NavigationFailureType,
4344
type RouteLocationNormalizedLoaded,
4445
type Router,
@@ -634,6 +635,78 @@ export function testDefineLoader<Context = void>(
634635
})
635636
}
636637

638+
describe('thrown errors in a aborted loader', () => {
639+
it(`navigation does not reject if the loader throws the passed signal, commit: ${commit}`, async () => {
640+
const loader = mockedLoader({
641+
key: 'id',
642+
commit,
643+
lazy: false,
644+
})
645+
646+
loader.spy.mockImplementation(
647+
async (
648+
_to: RouteLocationNormalizedLoaded,
649+
{ signal }: { signal?: AbortSignal }
650+
) => {
651+
await delay(10)
652+
signal?.throwIfAborted()
653+
return 'ko'
654+
}
655+
)
656+
657+
const { router } = singleLoaderOneRoute(loader.loader)
658+
const onError = vi.fn()
659+
router.onError(onError)
660+
661+
const navigationPromise = router.push('/fetch')
662+
// let the loaders start
663+
await vi.advanceTimersByTimeAsync(5)
664+
665+
await expect(router.push('/?other')).resolves.toBeUndefined()
666+
await vi.advanceTimersByTimeAsync(5)
667+
668+
const failure = await navigationPromise
669+
expect(failure).toBeDefined()
670+
expect(
671+
isNavigationFailure(failure, NavigationFailureType.aborted)
672+
).toBe(true)
673+
expect(router.currentRoute.value.fullPath).toBe('/?other')
674+
// the error was not propagated
675+
expect(onError).not.toHaveBeenCalled()
676+
})
677+
678+
it(`navigation rejects if the loader throws an error, commit: ${commit}`, async () => {
679+
const loader = mockedLoader({
680+
key: 'id',
681+
commit,
682+
lazy: false,
683+
})
684+
685+
loader.spy.mockImplementation(async () => {
686+
await delay(10)
687+
throw new Error('ko')
688+
})
689+
690+
const { router } = singleLoaderOneRoute(loader.loader)
691+
const onError = vi.fn()
692+
router.onError(onError)
693+
694+
const navigationPromise = expect(router.push('/fetch')).rejects.toThrow(
695+
'ko'
696+
)
697+
// let the loaders start
698+
await vi.advanceTimersByTimeAsync(5)
699+
700+
await expect(router.push('/?other')).resolves.toBeUndefined()
701+
await vi.advanceTimersByTimeAsync(5)
702+
703+
await navigationPromise
704+
expect(router.currentRoute.value.fullPath).toBe('/?other')
705+
// the error was not propagated
706+
expect(onError).toHaveBeenCalledTimes(1)
707+
})
708+
})
709+
637710
// https://github.com/posva/unplugin-vue-router/issues/584
638711
it(`skips child loaders if parent returns a NavigationResult, commit: ${commit}`, async () => {
639712
// Parent loader that redirects

0 commit comments

Comments
 (0)