-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
Falsy timer ID (0) prevents proper timeout/interval cleanup with custom timeout provider #10395
Description
Describe the bug
When using a custom timeout provider, timer IDs can legally be 0. However,
the codebase uses truthy checks before calling clearTimeout /
clearInterval. Since 0 is falsy, timers with ID 0
are never cleared.
This leads to stale timers continuing to run, causing unexpected refetches, extra updates, and lifecycle inconsistencies.
Custom timeout provider that returns 0:
// Custom timeout provider
let firstTimeout = true
let firstInterval = true
const timeoutProvider = {
setTimeout: (fn, ms) => {
if (firstTimeout) {
firstTimeout = false
return 0
}
return window.setTimeout(fn, ms)
},
clearTimeout: (id) => {
console.log('clearTimeout called with:', id)
window.clearTimeout(id)
},
setInterval: (fn, ms) => {
if (firstInterval) {
firstInterval = false
return 0
}
return window.setInterval(fn, ms)
},
clearInterval: (id) => {
console.log('clearInterval called with:', id)
window.clearInterval(id)
}
}
Use this provider with QueryClient / QueryObserver and trigger cleanup
(e.g., unsubscribe or option update). Timers with ID 0 will not be cleared.
Timers should always be cleared regardless of their numeric value.
Even when the timer ID is 0, cleanup functions should be called:
clearTimeout(0)
clearInterval(0)
Actual behavior
Timers with ID 0 are not cleared because of truthy checks like:
if (this.#gcTimeout) {
clearTimeout(this.#gcTimeout)
}
Since 0 is falsy, the cleanup logic is skipped.
Proposed fix
Replace truthy checks with explicit undefined checks:
// Before
if (this.#gcTimeout)
// After
if (this.#gcTimeout !== undefined)
Apply similar fixes to:
this.#staleTimeoutIdthis.#refetchIntervalId
Your minimal, reproducible example
Replace all truthy timer checks (e.g., if (timerId)) with explicit undefined checks (e.g., if (timerId !== undefined)) so timer ID 0 is handled correctly.
Steps to reproduce
- Create a custom timeout provider where
setTimeoutorsetIntervalreturns0 - Use it with
QueryClient/QueryObserver - Configure
staleTimeorrefetchIntervalso timers are scheduled - Trigger cleanup (unsubscribe or update options)
- Observe that
clearTimeout/clearIntervalare not called for ID0
Expected behavior
Timers should always be cleared regardless of their numeric value.
Even when the timer ID is 0, cleanup functions should be called:
clearTimeout(0)
clearInterval(0)
How often does this bug happen?
None
Screenshots or Videos
No response
Platform
- OS: Windows
- Browser: Chrome, Edge
- Runtime: Node.js
Tanstack Query adapter
None
TanStack Query version
5.96.2
TypeScript version
No response
Additional context
No response