AlgoInsight

Q&A

Change detection: When using setTimeout() is not your best option

By AlgoInsight Team2026-06-07concurrencyfrontendreactreact-hooksjavascript

Overview

setTimeout(fn, 0) (or some guessed delay) is frequently used to defer code until "after" something happens. The problem: you're hard-coding an assumption about when that something completes. On a slow device the timeout fires too early; on a fast one you've added needless lag. The robust approach is to listen for or await the real signal.

graph TD
    A[Need to run code after X happens] --> B{Why setTimeout?}
    B --> C[Wait for DOM element → MutationObserver / event]
    B --> D[Wait for framework render → lifecycle hook]
    B --> E[Wait for async result → Promise / await]
    B --> F[Wait for next paint → requestAnimationFrame]
    C & D & E & F --> G[React to the real event, not a guessed delay]
    style G fill:#1e1b4b,color:#fff

Why setTimeout Is Fragile

// ❌ Guessing that the data will be ready in 500ms
fetchData();
setTimeout(() => {
  render(data);    // might run before fetchData finished - race condition
}, 500);

The delay is a guess. It creates race conditions, wastes time when too long, and breaks when too short. Timing-based code is inherently unreliable across devices and network conditions.

Better Option 1: Promises / async-await (for async work)

// ✓ Run exactly when the data arrives
const data = await fetchData();
render(data);

Await the actual completion instead of guessing a duration.

Better Option 2: Framework lifecycle hooks (for render timing)

Frameworks tell you when rendering is done - use that instead of a timer.

React - run after render/commit with useEffect:

useEffect(() => {
  // runs after the DOM is updated
  inputRef.current?.focus();
}, [deps]);

Angular - for "after change detection settles," prefer ngAfterViewInit/afterNextRender over setTimeout hacks that force another cycle. Vue - await nextTick() runs code after the DOM updates:

await nextTick();
// DOM now reflects the latest state

Better Option 3: MutationObserver (for DOM changes)

Instead of polling with timeouts until an element appears, observe the DOM:

const observer = new MutationObserver((mutations, obs) => {
  const el = document.querySelector("#target");
  if (el) { handle(el); obs.disconnect(); }
});
observer.observe(document.body, { childList: true, subtree: true });

Better Option 4: Event listeners (for known events)

// ❌ setTimeout(() => {}, 1000) hoping the image loaded
// ✓ react to the load event
img.addEventListener("load", () => handleLoaded());

Better Option 5: requestAnimationFrame (for layout/paint timing)

For code that must run right before the next repaint (e.g. measuring layout, smooth animations), requestAnimationFrame is correct, not setTimeout:

requestAnimationFrame(() => {
  const height = element.offsetHeight;   // measured after layout, before paint
});

When setTimeout Is Appropriate

It's the right tool for genuinely time-based needs: debouncing/throttling intervals, intentional UX delays (e.g. auto-dismiss a toast after 3s), retry backoff, or yielding to the event loop deliberately. The lesson isn't "never use it" - it's "don't use it to guess at events that have their own signals."

Why This Matters in Interviews

This question probes whether you understand the event loop and reactive patterns versus timing hacks. Strong answers map each "waiting" scenario to its proper tool: async result → await; render done → lifecycle hook; DOM mutation → MutationObserver; known event → listener; paint → requestAnimationFrame. Recognizing that setTimeout creates race conditions and device-dependent bugs is the core insight.

Common Pitfalls

References

More answers