What a timer actually schedules

In a browser, setTimeout(callback, delay) asks the browser to run a callback once after at least roughly delay milliseconds. setInterval(callback, delay) asks it to run repeatedly. Both return an ID that you can pass to clearTimeout or clearInterval. You can try the examples below in a browser’s developer console; they require no libraries.

A timer is not a precise clock. When its delay has elapsed, the callback still has to wait until JavaScript can run it. A busy main thread, other queued work, and browser policies for inactive tabs can all make it run later than requested. A delay of zero does not interrupt the code currently running; it schedules the callback for a later turn.

For a one-time task, try: const id = setTimeout(() => console.log('ready'), 1000); To prevent it from running, call clearTimeout(id) before the callback starts. Once a callback has begun, clearing its ID cannot undo work it has already performed.

Choose an interval only when a steady request makes sense

An interval is convenient for simple repeated checks: const id = setInterval(() => console.log('check'), 1000); Call clearInterval(id) when the checks are no longer needed. Keep the ID somewhere the cleanup code can reach it. Losing the ID makes an unwanted interval harder to stop without reloading the page.

Intervals do not wait for asynchronous work started by their callbacks. If a callback starts a fetch request and returns, another interval callback may start a second request while the first is still pending. Even synchronous callbacks can run late when the page is busy. Do not assume an interval produces evenly spaced results.

If each run must finish before the next begins, schedule the next timeout after the work completes. For example, in a browser console: let stopped = false; let timerId; async function poll() { try { await doWork(); } finally { if (!stopped) timerId = setTimeout(poll, 1000); } } Here, doWork stands for an asynchronous function you provide. To stop future runs, use stopped = true; clearTimeout(timerId); This does not cancel doWork if it is already in progress; that requires a separate cancellation mechanism where the operation supports one.

Prevent duplicate timers and stale work

A common runaway-timer bug starts with a button or component that calls setInterval every time it opens, without clearing the previous interval. Each new timer continues independently. Store one ID, check whether a timer is already active before starting another, and clear it when the feature closes. For example: let tickId = null; function start() { if (tickId !== null) return; tickId = setInterval(() => console.log('tick'), 1000); } function stop() { if (tickId === null) return; clearInterval(tickId); tickId = null; } Calling start repeatedly now leaves just one active interval.

Also consider what the callback refers to. If it updates an element that has since been removed, the timer may keep doing useless work. Pair timer creation with cleanup in the same feature so its lifetime is easy to see. For a short-lived interface view, stop its timers when the view is removed or replaced; for an event-driven feature, stop them when the relevant event or state says the work is finished.

A self-scheduling timeout needs a stop condition too. Without one, it is effectively a repeating timer. Set a stopped flag before clearing its pending timeout so an asynchronous run that finishes later cannot schedule another one.

Match the timer to the job

Use setTimeout when you want one delayed action, such as hiding a brief message. For a search field, you can use it to debounce input: clear the previous timeout on each keystroke, then schedule the search after a quiet period. This avoids starting a new search for every character, though you may still need to handle results from requests already in flight.

Use setInterval when periodic attempts are acceptable and each callback is small. If timing depends on completion of asynchronous work, prefer a self-scheduling timeout. If you are updating a visual animation on screen, requestAnimationFrame is usually a better fit than a fixed timer because it schedules updates with the browser’s rendering cycle. It is not a substitute for a background clock or a request that must continue while the page is hidden.

For elapsed time, compare timestamps rather than counting callbacks. A display that increments a counter once per interval can drift when callbacks run late. Recording a start time and calculating elapsed time on each update keeps the displayed value tied to time passed, even if some updates are delayed.

A quick cleanup checklist

Before adding a timer, decide who owns it, what condition stops it, and whether another copy can be started. Save its ID, make starting it idempotent when appropriate, and clear it during cleanup. If the callback starts asynchronous work, decide whether overlapping runs are safe and whether that work needs its own cancellation.

Finally, test the stopping path as well as the starting path. Start a feature twice, close it, and check that its callback no longer runs. Try leaving the tab and returning without assuming the timer kept a perfect cadence. These checks catch many timer problems before a small convenience turns into unnecessary work.

Back to category ↗