setTimeout and setInterval in JavaScript

setTimeout runs a function once after a delay. setInterval runs it again and again until you stop it with the id it gave you.

setInterval runs a function every so many milliseconds until you stop it. setTimeout runs a function once, after a delay. Both return a number, the timer id, and that id is the only way to stop the timer: clearInterval(id) or clearTimeout(id).

const id = setInterval(() => console.log('tick'), 1000); // every 1000 ms
setTimeout(() => clearInterval(id), 5000);               // stop it after 5 s

Try both below. Each button logs the id it got back. Start the interval twice, then press clearInterval, and watch one interval keep going.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Timer lab</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 8px; }
  button {
    font: inherit; font-size: 14px; padding: 8px 12px; border-radius: 8px;
    border: 1px solid #c9ced6; background: #fff; cursor: pointer;
  }
  button.go { border-color: #16a34a; color: #0f5132; }
  button.stop { border-color: #ea580c; color: #9a3412; }
  .status { font-size: 14px; margin: 6px 0 8px; }
  .status b { font-variant-numeric: tabular-nums; }
  #log {
    height: 230px; overflow-y: auto; margin: 0; padding: 10px 12px;
    background: #fff; border: 1px solid #e1e4ea; border-radius: 10px;
    font: 13px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap;
  }
</style>
</head>
<body>
<div class="row">
  <button class="go" id="once">setTimeout 2 s</button>
  <button class="stop" id="cancelOnce">clearTimeout</button>
</div>
<div class="row">
  <button class="go" id="start">setInterval 1 s</button>
  <button class="stop" id="stop">clearInterval</button>
  <button class="stop" id="all">Clear all</button>
</div>
<div class="status">Running intervals: <b id="count">0</b> &middot; saved id: <b id="saved">none</b></div>
<pre id="log"></pre>

<script>
  const log = document.getElementById('log');
  let timeoutId = null;   // id from the last setTimeout
  let intervalId = null;  // id from the last setInterval
  let running = [];       // every interval started, to show the "lost id" bug

  function write(text) {
    log.textContent += text + '\n';
    log.scrollTop = log.scrollHeight;
  }
  function show() {
    document.getElementById('count').textContent = running.length;
    document.getElementById('saved').textContent = intervalId ?? 'none';
  }

  document.getElementById('once').addEventListener('click', () => {
    timeoutId = setTimeout(() => {
      write('timeout ' + timeoutId + ' ran once');
      timeoutId = null;
    }, 2000);
    write('setTimeout returned id ' + timeoutId);
  });

  document.getElementById('cancelOnce').addEventListener('click', () => {
    if (timeoutId === null) return write('no pending timeout to clear');
    clearTimeout(timeoutId);
    write('clearTimeout(' + timeoutId + '): it will not run');
    timeoutId = null;
  });

  document.getElementById('start').addEventListener('click', () => {
    // No guard on purpose: a second click starts a second interval
    // and overwrites intervalId, so the first id is lost.
    let n = 0;
    const id = setInterval(() => write('interval ' + id + ' tick ' + (++n)), 1000);
    intervalId = id;
    running.push(id);
    write('setInterval returned id ' + id);
    show();
  });

  document.getElementById('stop').addEventListener('click', () => {
    if (intervalId === null) return write('no saved id: ' + running.length + ' still running');
    clearInterval(intervalId);
    running = running.filter((id) => id !== intervalId);
    write('clearInterval(' + intervalId + ')');
    intervalId = null;
    if (running.length) write('still running: ' + running.join(', ') + ' (their ids were overwritten)');
    show();
  });

  // The fix for lost ids: keep every id, clear every one.
  document.getElementById('all').addEventListener('click', () => {
    running.forEach((id) => clearInterval(id));
    write('cleared ' + (running.length || 'no') + ' interval(s)');
    running = [];
    intervalId = null;
    show();
  });

  write('Click a green button. Try starting the interval twice.');
</script>
</body>
</html>
setTimeout, setInterval and the ids they return. Starting an interval twice loses the first id.

Syntax and the delay in milliseconds

Both functions take the same arguments:

setTimeout(callback, delayMs, arg1, arg2);
setInterval(callback, delayMs, arg1, arg2);
  • callback is a function. Pass the function itself, update, not update(). With the brackets, it runs right away and the timer gets its return value instead.
  • delayMs is in milliseconds: 1000 is one second, 60000 one minute. Leave it out and it counts as 0.
  • Extra arguments after the delay are passed to the callback. setTimeout(search, 300, text) calls search(text).

You can also pass a string of code instead of a function. It works like eval, and pages with a strict Content Security Policy block it, so pass a function.

The id, clearTimeout and clearInterval

Every call returns a new positive integer. Keep it in a variable that the stop code can see, usually one declared outside the click handlers.

setTimeout runs once, setInterval repeats, and a setTimeout that schedules itself waits for each run to finish.
setTimeout runs once, setInterval repeats, and a setTimeout that schedules itself waits for each run to finish.
let timer = null;

function start() {
  clearInterval(timer);            // never run two at once
  timer = setInterval(update, 1000);
}

function stop() {
  clearInterval(timer);
  timer = null;
}

Passing an id that is not running, or null, does nothing and throws no error. That is why the first line of start() is safe, and why a wrong id fails silently.

Timers are not exact

The delay is a minimum, not a promise. A timer runs only when the page is free, so a slow script, a long loop or a busy page makes it late.

The HTML standard also sets a floor of 4 ms once timers are nested more than five levels deep.

Browsers also slow down timers in tabs that are hidden or in the background, to save power. An interval set to one second can run much less often there.

So a clock that adds 1 on every tick goes wrong. Press Freeze the page below and compare the two clocks.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Counting ticks vs reading the clock</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .clocks { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
  .clock { background: #fff; border-radius: 12px; padding: 12px; border: 2px solid #e1e4ea; }
  .clock.bad { border-color: #fdba74; }
  .clock.good { border-color: #86efac; }
  .clock h3 { margin: 0 0 4px; font-size: 14px; }
  .clock code { font-size: 12px; color: #4b5563; }
  .time { font: 700 40px/1.2 ui-monospace, Consolas, monospace; margin: 8px 0 0; font-variant-numeric: tabular-nums; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 8px; }
  button { font: inherit; font-size: 14px; padding: 8px 12px; border-radius: 8px; border: 1px solid #c9ced6; background: #fff; cursor: pointer; }
  #gap { font-size: 14px; margin: 0; }
  #gap b { font-variant-numeric: tabular-nums; }
</style>
</head>
<body>
<div class="clocks">
  <div class="clock bad">
    <h3>Counts ticks</h3>
    <code>seconds += 1</code>
    <div class="time" id="counted">0 s</div>
  </div>
  <div class="clock good">
    <h3>Reads Date.now()</h3>
    <code>now - start</code>
    <div class="time" id="measured">0 s</div>
  </div>
</div>
<div class="row">
  <button id="block">Freeze the page for 2.5 s</button>
  <button id="reset">Reset</button>
</div>
<p id="gap">Behind by <b id="behind">0</b> s. Freeze the page and watch the left clock fall behind.</p>

<script>
  let start = Date.now();
  let seconds = 0;

  // Wrong: assumes every tick arrives, exactly 1000 ms apart.
  setInterval(() => {
    seconds += 1;
    document.getElementById('counted').textContent = seconds + ' s';
  }, 1000);

  // Right: the timer only says "update now"; the time comes from the clock.
  setInterval(() => {
    const real = Math.floor((Date.now() - start) / 1000);
    document.getElementById('measured').textContent = real + ' s';
    document.getElementById('behind').textContent = Math.max(0, real - seconds);
  }, 200);

  // Busy work that blocks the page, like a slow script or a background tab.
  document.getElementById('block').addEventListener('click', () => {
    const until = Date.now() + 2500;
    while (Date.now() < until) { /* no timer can run during this loop */ }
  });

  document.getElementById('reset').addEventListener('click', () => {
    start = Date.now();
    seconds = 0;
    document.getElementById('counted').textContent = '0 s';
  });
</script>
</body>
</html>
Left: seconds += 1 on each tick. Right: Date.now() minus the start time. Freezing the page costs the left clock a second.
While the page is busy, ticks come late and missed ones are not made up. Reading the clock fixes the next update.
While the page is busy, ticks come late and missed ones are not made up. Reading the clock fixes the next update.

The fix is to use the timer only as a reminder to repaint, and to read the real time each time:

const start = Date.now();
setInterval(() => {
  const seconds = Math.floor((Date.now() - start) / 1000);
  clock.textContent = seconds + ' s';
}, 250);

A shorter interval such as 250 ms makes the display change close to the real second. For a countdown to a date, the same idea is built out in the HTML countdown timer guide.

setInterval or a setTimeout that calls itself

A setTimeout can schedule the next run at the end of each run:

function poll() {
  checkForUpdates().then(() => {
    timer = setTimeout(poll, 5000); // next check 5 s after this one finished
  });
}
poll();
setInterval setTimeout that calls itself
Next run Keeps its schedule, even if the last run started async work that is still going Scheduled when the last run finishes
Slow or async work Runs can overlap, such as two requests at once Never overlaps
Change the delay Clear and start again Pass a new delay each time
Stop it clearInterval(id) clearTimeout(id), or just do not schedule the next one

For a display that repaints, setInterval is fine. For work that waits on the network, the self-scheduling setTimeout is safer.

The this problem

When you pass a method, only the function goes to the timer, not the object it belongs to:

const player = {
  name: 'Ada',
  hello() { console.log(this.name); },
};

setTimeout(player.hello, 1000);          // this is not player
setTimeout(() => player.hello(), 1000);  // logs "Ada"
setTimeout(player.hello.bind(player), 1000); // also "Ada"

Wrap the call in an arrow function, or use bind. The same applies to class methods passed as callbacks.

setTimeout(fn, 0) runs after the current code

A zero delay does not mean "now". The callback waits until the code that is running finishes. Promise callbacks that are already queued also go first.

setTimeout(() => console.log('3: timeout'), 0);
Promise.resolve().then(() => console.log('2: promise'));
console.log('1: now');

This prints 1, 2, 3. It is a way to run something after the current work finishes, not a way to make it faster.

Debounce: wait until typing stops

A search box that searches on every key does five searches for "berry". A debounce waits until the typing pauses:

let searchTimer;
input.addEventListener('input', () => {
  clearTimeout(searchTimer);                      // cancel the last plan
  searchTimer = setTimeout(search, 300, input.value);
});
Each key cancels the waiting timer and starts a new one. Only the last one finishes.
Each key cancels the waiting timer and starts a new one. Only the last one finishes.

The same setTimeout and clearTimeout pair also works for saving drafts, resizing charts after the window stops changing size, and hiding a tooltip after a short pause.

A finished example: debounced search and a focus timer

The search box below runs one search after you stop typing. The focus timer is a Pomodoro-style timer that pauses and resumes. It never counts ticks. It stores the moment it should end and paints whatever is left:

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Debounced search and focus timer</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  section { background: #fff; border: 1px solid #e1e4ea; border-radius: 12px; padding: 12px 14px; margin-bottom: 12px; }
  h3 { margin: 0 0 8px; font-size: 15px; }
  input { width: 100%; box-sizing: border-box; font: inherit; font-size: 16px; padding: 8px 10px; border: 1px solid #c9ced6; border-radius: 8px; }
  .meta { font-size: 13px; color: #4b5563; margin: 6px 0; }
  .meta b { color: #1d2330; font-variant-numeric: tabular-nums; }
  ul { margin: 0; padding-left: 18px; font-size: 14px; min-height: 60px; max-height: 110px; overflow-y: auto; }
  .time { font: 700 48px/1.1 ui-monospace, Consolas, monospace; text-align: center; margin: 4px 0 10px; font-variant-numeric: tabular-nums; }
  .time.done { color: #16a34a; }
  .row { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
  button { font: inherit; font-size: 14px; padding: 8px 14px; border-radius: 8px; border: 1px solid #c9ced6; background: #fff; cursor: pointer; }
  button.main { background: #16a34a; border-color: #16a34a; color: #fff; min-width: 90px; }
</style>
</head>
<body>
<section>
  <h3>Search, debounced</h3>
  <input id="q" type="search" placeholder="Type a fruit, e.g. berry" autocomplete="off">
  <p class="meta">Keystrokes: <b id="keys">0</b> &middot; searches run: <b id="runs">0</b></p>
  <ul id="results"></ul>
</section>

<section>
  <h3>Focus timer</h3>
  <div class="time" id="time">25:00</div>
  <div class="row">
    <button class="main" id="toggle">Start</button>
    <button id="reset">Reset</button>
    <button data-min="25">25 min</button>
    <button data-min="5">5 min</button>
  </div>
</section>

<script>
  /* 1. Debounce: wait until typing pauses for 300 ms, then search once. */
  const fruits = ['apple', 'apricot', 'banana', 'blackberry', 'blueberry', 'cherry',
    'cranberry', 'grape', 'lemon', 'lime', 'mango', 'orange', 'peach', 'pear', 'raspberry', 'strawberry'];
  const q = document.getElementById('q');
  let keys = 0, runs = 0, searchTimer;

  function show(text) {
    const hits = fruits.filter((f) => f.includes(text.trim().toLowerCase()));
    document.getElementById('results').innerHTML = hits.map((f) => '<li>' + f + '</li>').join('');
  }

  function search(text) {
    document.getElementById('runs').textContent = ++runs;
    show(text);
  }

  q.addEventListener('input', () => {
    document.getElementById('keys').textContent = ++keys;
    clearTimeout(searchTimer);                           // cancel the search planned by the last key
    searchTimer = setTimeout(search, 300, q.value);      // extra argument is passed to search()
  });

  /* 2. Focus timer: the interval only repaints; the time comes from Date.now(). */
  const time = document.getElementById('time');
  const toggle = document.getElementById('toggle');
  let length = 25 * 60 * 1000;  // chosen length in ms
  let remaining = length;       // ms left while paused
  let endAt = null;             // timestamp when the timer finishes, null when paused
  let tick = null;

  function paint() {
    const ms = endAt ? Math.max(0, endAt - Date.now()) : remaining;
    const s = Math.ceil(ms / 1000);
    time.textContent = String(Math.floor(s / 60)).padStart(2, '0') + ':' + String(s % 60).padStart(2, '0');
    time.classList.toggle('done', ms === 0);
    if (endAt && ms === 0) pause();
  }

  function start() {
    if (remaining === 0) return;
    endAt = Date.now() + remaining;
    clearInterval(tick);          // never keep two intervals
    tick = setInterval(paint, 250);
    toggle.textContent = 'Pause';
  }

  function pause() {
    if (endAt) remaining = Math.max(0, endAt - Date.now());
    endAt = null;
    clearInterval(tick);
    tick = null;
    toggle.textContent = remaining === 0 ? 'Done' : 'Resume';
    paint();
  }

  toggle.addEventListener('click', () => (endAt ? pause() : start()));
  document.getElementById('reset').addEventListener('click', () => {
    pause();
    remaining = length;
    toggle.textContent = 'Start';
    paint();
  });
  document.querySelectorAll('[data-min]').forEach((b) => b.addEventListener('click', () => {
    length = b.dataset.min * 60 * 1000;
    document.getElementById('reset').click();
  }));

  show('');
</script>
</body>
</html>
Debounced search, and a timer computed from timestamps. Pause, wait, resume: no seconds are lost or gained.
  1. Start: save endAt = Date.now() + remaining and start an interval that only repaints.
  2. Each tick: show endAt - Date.now(), rounded up to whole seconds.
  3. Pause: save remaining = endAt - Date.now() and call clearInterval.
  4. Before starting again: clear the old interval so two never run at once.

Because the time comes from the clock, a slow tab only delays the repaint. The number is right as soon as it draws. The ids and elements used here are found with querySelector and getElementById.

requestAnimationFrame for animation

For movement on screen, an interval such as setInterval(move, 16) is not tied to when the screen actually redraws. requestAnimationFrame calls your function right before the next paint and passes a timestamp:

function frame(now) {
  box.style.transform = 'translateX(' + (now / 10 % 300) + 'px)';
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);

Compute the position from the timestamp, as with the clock, so the speed stays the same whatever the frame rate. Stop it with cancelAnimationFrame(id). For simple movement, a CSS animation needs no script at all.

When it does not work

What you see Cause Fix
The interval never stops It was started twice and the first id was overwritten clearInterval(timer) before every new setInterval
clearInterval does nothing The id lives in a local variable the stop code cannot see Declare let timer outside both functions
The function runs at once, then never again setTimeout(update(), 1000) passes the result, not the function setTimeout(update, 1000)
The clock falls behind It adds 1 per tick, and late or missed ticks add up Compute from Date.now()
Timers are slow in a background tab The browser throttles hidden tabs Compute from timestamps and repaint when the tab is visible again
this is undefined or the wrong object in the callback The method was passed without its object Arrow function or bind
Requests pile up setInterval starts a new request before the last one ends A setTimeout that schedules itself

If nothing runs at all, the script itself may not be loading. HTML JavaScript not working covers those causes.

A timer is hard to show in a screenshot. The point is that it moves, pauses and resumes. An .html attachment may open as plain code on a phone, or not at all.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can start and pause the timer themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is the difference between setTimeout and setInterval?

setTimeout calls the function once, after at least the delay you give it. setInterval keeps calling it every delay milliseconds until you pass its id to clearInterval. Both return an id, and both take the delay in milliseconds.

Why is clearInterval not working?

Usually the id you pass is not the id of the running interval. Common causes: the interval was started twice and the variable now holds only the second id, the id was stored in a local variable that the stop code cannot see, or the code called clearInterval on a different variable.

Is setInterval accurate?

No. A timer never fires early, but it can fire late, for example while other code is running or while the tab is in the background. Use the timer only as a signal to update, and read the real time from Date.now() or performance.now().

What does setTimeout(fn, 0) do?

It runs fn as soon as possible after the current code has finished, not immediately. Promise callbacks that are already waiting run before it. It is a way to let the current work and any pending updates finish first.

Should I use setInterval for animation?

Use requestAnimationFrame instead. It calls your function right before the browser paints the next frame, gives you a timestamp to compute positions from, and browsers pause or slow it while the tab is hidden.

Keep reading

Web Workers in JavaScript: run heavy work without freezing the pageRun heavy JavaScript in a Web Worker so the page never freezes. One-file setup with a Blob UBuild a stopwatch in HTML and JavaScriptBuild a stopwatch in HTML and JavaScript with performance.now(), pause and resume, lap timesAsync and await in JavaScript, with live examplesHow async and await work in JavaScript: what a Promise is, try/catch, Promise.all vs allSettrequestAnimationFrame in JavaScript: build a smooth animation loopHow requestAnimationFrame works: the timestamp, speed that does not depend on frame rate, caFormat a date in JavaScriptFormat dates in JavaScript for people and for machines: toLocaleDateString options, Intl.DataddEventListener in JavaScript, with live examplesHow addEventListener works: vs onclick, target vs currentTarget, bubbling and capture, once,HTML countdown timerA working HTML countdown timer in about twenty lines, the time zone mistake that shifts it bA free HTML clock widget you can paste anywhereCopy-paste code for a live clock, a timezone board and a countdown. No account, no third-parThe DOM in HTML and JavaScript: the page as a tree you can changeWhat the DOM is, how it differs from your HTML source, and how JavaScript selects, creates, querySelector and querySelectorAll: find elements with CSS selectorsFind elements with CSS selectors in JavaScript: by id, class, attribute or state. Try selectHTML JavaScript not workingJavaScript in your HTML file does nothing. Check the console, then script order, then the fiHTML to linkTurn HTML into a link in 5 steps: check the page, paste it into a NOS document, copy the sha