Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/angular-inject-queries-throw-on-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/angular-query-experimental': patch
---

fix(angular-query): honor per-query 'throwOnError' in 'injectQueries'
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,103 @@ describe('injectQueries', () => {
expect(rendered.getByText('status2: success, data2: 2')).toBeInTheDocument()
})

describe('throwOnError', () => {
it('should throw when throwOnError is true', async () => {
const key1 = queryKey()
const key2 = queryKey()

TestBed.runInInjectionContext(() =>
injectQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => sleep(0).then(() => 1),
},
{
queryKey: key2,
queryFn: () =>
sleep(0).then(() => Promise.reject(new Error('Some error'))),
throwOnError: true,
},
],
})),
)

await expect(vi.runAllTimersAsync()).rejects.toThrow('Some error')
})

it('should keep the queries observable after throwing', async () => {
const key1 = queryKey()
const key2 = queryKey()

const result = TestBed.runInInjectionContext(() =>
injectQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => sleep(10).then(() => 1),
},
{
queryKey: key2,
queryFn: () =>
sleep(20).then(() => Promise.reject(new Error('Some error'))),
retry: false,
throwOnError: true,
},
],
})),
)

await vi.advanceTimersByTimeAsync(11)
expect(result()[0].data()).toBe(1)

await expect(vi.advanceTimersByTimeAsync(10)).rejects.toThrow(
'Some error',
)

// `throwOnError` means "also throw", so the results the caller renders
// from must still be updated - including the sibling that succeeded.
expect(result()[0].data()).toBe(1)
expect(result()[1].status()).toBe('error')
expect(result()[1].error()).toEqual(Error('Some error'))
})

it('should evaluate throwOnError with the error and query of the failing query only', async () => {
const key1 = queryKey()
const key2 = queryKey()
const boundaryFn = vi.fn().mockReturnValue(false)

TestBed.runInInjectionContext(() =>
injectQueries(() => ({
queries: [
{
queryKey: key1,
queryFn: () => sleep(10).then(() => 1),
},
{
queryKey: key2,
queryFn: () =>
sleep(10).then(() => Promise.reject(new Error('Some error'))),
retry: false,
throwOnError: boundaryFn,
},
],
})),
)

await vi.advanceTimersByTimeAsync(11)

expect(boundaryFn).toHaveBeenCalledTimes(1)
expect(boundaryFn).toHaveBeenCalledWith(
Error('Some error'),
expect.objectContaining({
queryKey: key2,
state: expect.objectContaining({ status: 'error' }),
}),
)
})
})

describe('isRestoring', () => {
it('should not fetch for the duration of the restoring period when isRestoring is true', async () => {
const key1 = queryKey()
Expand Down
26 changes: 26 additions & 0 deletions packages/angular-query-experimental/src/inject-queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
QueriesObserver,
QueryClient,
notifyManager,
shouldThrowError,
} from '@tanstack/query-core'
import {
DestroyRef,
Expand Down Expand Up @@ -301,7 +302,32 @@ export function injectQueries<
: ngZone.runOutsideAngular(() =>
observer.subscribe(
notifyManager.batchCalls((state) => {
// Publish first: `throwOnError` means "also throw", so the
// results the caller renders from - the failing query's own
// error state included - must still be updated. Nothing below
// may leave the signal holding a stale notification.
resultFromSubscriberSignal.set(getCombinedResult(state))

// Each query carries its own `throwOnError`, so the option is
// resolved per query against that query's own error.
const queryObservers = observer.getObservers()
const resultToThrow = state.find((result, index) => {
const queryObserver = queryObservers[index]
return (
result.isError &&
!result.isFetching &&
!!queryObserver &&
shouldThrowError(queryObserver.options.throwOnError, [
result.error,
queryObserver.getCurrentQuery(),
])
)
})

if (resultToThrow) {
ngZone.onError.emit(resultToThrow.error)
throw resultToThrow.error
}
}),
),
)
Expand Down