Implement a polyfill for setTimeout
There's no pure-JS way to create a real timer — setTimeout is a host API. The realistic 'polyfill' is a wrapper that adds features (cancellable handle, arg forwarding) or a custom scheduler built on requestAnimationFrame comparing timestamps. The interview is about understanding the event loop and that timers are host-provided.
This is partly a trick question: you can't implement setTimeout in pure JavaScript — there's no language primitive for "wait N ms." Timers are a host API (browser / Node), backed by the event loop and the OS. So the answer is one of two things, and you should say which you're doing.
Interpretation 1: a wrapper that adds features
If "polyfill" means "reimplement the API surface with extras":
function customSetTimeout(callback, delay = 0, ...args) {
let cancelled = false;
// still delegates to the host timer — there's no other source of "wait"
const id = setTimeout(() => {
if (!cancelled) callback(...args);
}, delay);
return {
clear() { cancelled = true; clearTimeout(id); },
};
}You're wrapping the host API to return a richer handle, forward args, etc. — but the actual waiting is still the host's job.
Interpretation 2: a custom scheduler on rAF
If you genuinely can't use setTimeout and want to simulate it, build a scheduler on requestAnimationFrame (also a host API, but a different one) that compares timestamps:
function rafTimeout(callback, delay) {
const start = performance.now();
let rafId;
function check(now) {
if (now - start >= delay) callback();
else rafId = requestAnimationFrame(check);
}
rafId = requestAnimationFrame(check);
return () => cancelAnimationFrame(rafId);
}This approximates it — but it's frame-bound (~16ms granularity), pauses in background tabs, and still relies on a host API. It proves the point: every approach delegates to some host capability.
What the interview is really testing
The event loop. setTimeout(fn, delay) doesn't run fn after exactly delay ms — it queues fn as a macrotask after at least delay ms; it runs once the call stack is clear and microtasks have drained. delay is a minimum, not a guarantee. Knowing that — and that timers are host-provided, not language features — is the actual deliverable.
The framing
"Strictly, you can't — setTimeout is a host API, not a JS language feature; there's no pure-JS way to 'wait.' So I'd clarify the intent: either a wrapper that adds a cancellable handle and arg forwarding while still delegating the wait to the host timer, or a requestAnimationFrame-based scheduler that approximates it by comparing timestamps. Both still lean on a host API. The real content is the event loop: setTimeout queues a macrotask after at least the delay — it's a minimum, not a precise schedule."
Follow-up questions
- •Why is setTimeout's delay a minimum, not a guarantee?
- •Where does setTimeout's callback go in the event loop?
- •What's the difference between a macrotask and a microtask?
- •What does setTimeout(fn, 0) actually do?
Common mistakes
- •Claiming you can implement a real timer in pure JS.
- •Not mentioning that timers are host-provided.
- •Thinking the delay is exact.
- •Not connecting it to the event loop / macrotask queue.
Performance considerations
- •Timer callbacks are macrotasks — they wait behind the current stack and all microtasks. Heavy synchronous work or microtask floods delay them. rAF-based approximations are capped at frame rate and pause when the tab is hidden.
Edge cases
- •setTimeout(fn, 0) still waits for the stack and microtasks to clear.
- •Nested timeouts clamped to a minimum (~4ms) by browsers.
- •Background tabs throttling timers to ~1s.
- •Long synchronous work delaying the callback well past the delay.
Real-world examples
- •Wrapping setTimeout to return a cancellable handle in utility libraries.
- •Explaining to a teammate why a setTimeout(fn, 100) fired 300ms late under load.