Q&A
Change detection: When using setTimeout() is not your best option
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
- Using
setTimeout(fn, 0)to wait for a render instead of a lifecycle hook. - Polling with repeated timeouts where an observer/event would react instantly.
- Race conditions when the guessed delay is shorter than the real operation.
- In Angular, sprinkling
setTimeoutto "fix" change-detection issues instead of using proper hooks orNgZone.
References
- MDN
setTimeout: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout - MDN
MutationObserver: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver