Skip to content

Falsy timer ID (0) prevents proper timeout/interval cleanup with custom timeout provider #10395

@mukeshdhadhariya

Description

@mukeshdhadhariya

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.#staleTimeoutId
  • this.#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

  1. Create a custom timeout provider where setTimeout or setInterval returns 0
  2. Use it with QueryClient / QueryObserver
  3. Configure staleTime or refetchInterval so timers are scheduled
  4. Trigger cleanup (unsubscribe or update options)
  5. Observe that clearTimeout / clearInterval are not called for ID 0

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions