A status poller runs setInterval(() => refresh(), 1000), where refresh() is asynchronous and usually takes about three seconds. What goes wrong, and what would you use instead?
answer
- the timer never sees the promise
- concurrency scales with latency
- stale response overwrites fresh state
- schedule from completion, not a clock
- one decision point per cycle
basics
~20 sThe timer ignores whatever refresh() returns, so it starts a new one every second regardless: about three run concurrently, and the count grows if refresh slows down. Replace the interval with a self-rescheduling setTimeout armed only after each run settles.
solid answer
~50 s`setInterval` knows nothing about promises. The callback returns immediately after kicking off the asynchronous work, so a new `refresh()` starts every second while roughly three earlier ones are still outstanding. That is bad in three ways: concurrency scales with how slow the operation is, so a struggling backend gets *more* load exactly when it needs less; responses can settle out of order, letting a stale one overwrite fresher state; and there is no place to handle a failure before the next attempt fires. The fix is to let completion drive the schedule — run `refresh()`, and only when it settles arm the next run with `setTimeout`, so there is always at most one in flight and a guaranteed gap between them. That also gives you the natural hook for backoff and jitter on failure. The minimal patch, if you must keep the interval, is an in-flight boolean that makes a tick skip itself while a previous run is outstanding.
code
javascript · 10 linesfunction refresh() {
return new Promise((resolve) => setTimeout(resolve, 3000));
}
let inFlight = 0;
setInterval(() => {
inFlight += 1;
console.log('in flight:', inFlight);
refresh().finally(() => { inFlight -= 1; });
}, 1000);go deeper
Know that a timer callback returning a promise tells the timer nothing, so an asynchronous poller on setInterval starts a new operation every period whether or not the previous one has finished.
Work out how many operations end up in flight from the period and the duration, and rewrite the poller so the next run is armed from the settled path rather than by the clock.
Name the operational failure: concurrency proportional to latency means the client pushes hardest during a degradation, and out-of-order settling silently corrupts state. Add backoff, jitter, and bounded stop behaviour.
Own the polling policy across clients — cadence, backoff and jitter as a shared default, and when polling should be replaced by a push mechanism at all rather than tuned.
## What setInterval knows about your callback: nothing A timer invokes the callback and is done with it. It does not inspect the return value, and it has no concept of the work being "finished". So for a callback whose body starts asynchronous work and returns immediately, the timer's period is the rate at which work is *started*, not the rate at which it *completes*. Marking the callback `async` changes nothing — it only makes the callback return a promise the timer discards. ## The pile-up With a 1000 ms period and a 3000 ms operation, the steady state is about three operations in flight at all times. The number is `duration / period`, and that is the dangerous part: it is not a constant you chose, it is a function of how slow the dependency happens to be right now. If the operation degrades to 10 seconds, ten are in flight. If it degrades to 60, sixty are. The poller applies *more* pressure precisely when whatever it is polling is least able to take it — a self-reinforcing loop that turns a slow dependency into an unavailable one. ## The secondary damage Even when the load is survivable, overlap breaks correctness: - **Out-of-order settling.** Nothing guarantees the request started first settles first. A slow older response can land after a fast newer one and overwrite fresh state with stale state — a flickering or plainly wrong UI whose cause is invisible in the code that renders it. - **No failure handling.** Each tick is an independent invocation, so an error in one has no bearing on the next. If the operation is failing, the timer keeps firing at full rate, generating an error every second indefinitely. - **Unbounded resource use.** In-flight operations hold connections, buffers, and closures. A poller left running while the dependency hangs can accumulate them without limit, because the only thing throttling it is a clock. ## Fix 1: the in-flight guard The one-line patch keeps the interval and makes ticks skip themselves: ```js let busy = false; setInterval(() => { if (busy) return; // drop this tick, one is still running busy = true; refresh().catch(report).finally(() => { busy = false; }); }, 1000); ``` This bounds concurrency at one, which is the important property. Its weakness is that spacing is now unpredictable — if a run finishes just before a tick, the next starts almost immediately with no gap at all. ## Fix 2: let completion drive the schedule The better shape drops `setInterval` entirely. Run the operation; when it settles, arm the next run: ```js let timer = null; let stopped = false; function schedule(delay) { timer = setTimeout(run, delay); } function run() { refresh() .catch(report) .finally(() => { if (!stopped) schedule(1000); }); } schedule(0); function stop() { stopped = true; clearTimeout(timer); } ``` Now there is at most one operation outstanding, a guaranteed gap between runs, and — crucially — a single place where the next delay is chosen. The `finally` matters: schedule from the settled path, not from the success path, or one failure silently ends the poller forever. ## The delay is now a policy, not a constant Because the delay is computed per cycle, the same shape absorbs everything a real poller needs: exponential backoff after an error (`delay = Math.min(delay * 2, 30_000)`) with a reset on success; random jitter so that many clients recovering together do not resynchronise into a thundering herd; and a slower cadence when the data is unchanged. None of that is expressible with `setInterval`, whose delay is fixed at registration. ## Stopping cleanly With `setInterval` you hold one handle for the timer's life. With the self-rescheduling form each cycle produces a new handle, so stopping needs both a flag consulted before arming the next run and a `clearTimeout` for the one already pending. That is slightly more code, and it is the price of having a decision point every cycle. ## The interview signal A candidate who says only "await it inside the callback" has missed the point — awaiting inside a `setInterval` callback delays nothing, because the timer never looks at the promise. The signal is recognising that the schedule must be driven by completion rather than by a clock, and naming the failure mode by its shape: concurrency proportional to latency, which is the wrong direction.
- Does marking the setInterval callback async and awaiting inside it prevent the overlap?No. An async function returns a promise as soon as it hits its first await, and the timer discards that promise — it has no mechanism to wait on a return value. The callback still returns immediately from the timer's point of view, so the next tick fires exactly on schedule. Only completion-driven scheduling actually bounds concurrency.
- Why is concurrency that scales with the operation's latency worse than simply picking a too-short interval?Because it removes the ceiling. A fixed too-short interval applies a constant, known load. With overlap, the number in flight is duration divided by period, so the load grows exactly as the dependency slows — you push hardest when it can take it least, which turns a degradation into an outage.
- Once completion drives the schedule, what would you add before shipping the poller?Backoff and jitter. Double the delay on failure up to a cap and reset it on success, so a failing dependency is not hammered, and add a random component so many clients that failed together do not resynchronise and arrive as one burst on recovery. Both are one-liners at the point where the next delay is chosen.
saying these in an interview costs you the question
- Says setInterval waits for a returned promise
- Thinks an async callback makes the timer await it
- Fixes it by raising the interval to the slowest observed duration
- Ignores that responses can settle out of order
- Schedules the next run only on success, so an error stops the poller