Timers

Learn how to schedule code to run after a delay or repeatedly using setTimeout and setInterval, how to cancel them, and understand their scheduling limitations.

Not everything in JavaScript needs to happen immediately. Animations, polling for updates, debounced search inputs, and countdown timers all rely on scheduling code to run later or repeatedly. JavaScript provides two core timer functions for this: setTimeout() for a one-time delayed call, and setInterval() for a repeating call.

setTimeout — run once, after a delay

setTimeout(callback, delayInMs) schedules callback to run once, after at least delayInMs milliseconds have passed. It returns a numeric timer ID immediately, without blocking the rest of the script — the code after setTimeout() keeps running right away.

The delay is a minimum, not a guarantee. JavaScript runs on a single thread with an event loop, so if the thread is busy running other code when the delay expires, the callback waits until the thread is free.

setInterval — run repeatedly

setInterval(callback, intervalInMs) works like setTimeout but keeps calling callback again and again, roughly every intervalInMs milliseconds, until it is explicitly stopped. It also returns a timer ID immediately.

Like setTimeout, the interval is a minimum spacing, not an exact guarantee — if the callback itself takes longer than the interval to run, or if the thread is busy, calls can be delayed or, in some environments, skipped.

clearTimeout and clearInterval

Both setTimeout() and setInterval() return an ID that identifies that specific scheduled timer. Passing that ID to clearTimeout(id) or clearInterval(id) respectively cancels it — the callback will not run (for a pending setTimeout) or will stop running again (for setInterval).

It's important to use the matching function for the matching timer type conceptually (clearTimeout for setTimeout IDs, clearInterval for setInterval IDs), even though in most browser environments the two functions are interchangeable under the hood — relying on that is not good practice.

Scheduling behavior and limitations

Timers do not pause the rest of your code — setTimeout(fn, 1000) does not make the program wait one second before continuing; it schedules fn to run later while execution continues immediately past the setTimeout() call.

A delay of 0 (setTimeout(fn, 0)) does not run fn synchronously or 'right now' — it still queues fn to run after the current script finishes and the event loop is free, which is why it's often used to defer work until after the current execution finishes.

Browsers commonly throttle timers in inactive (background) tabs to save battery and CPU, meaning a setInterval() in a tab you've switched away from may fire much less often than requested. Deeply nested setTimeout calls (more than a few levels) are also often clamped to a minimum delay (historically 4ms) by browsers.

Scheduling and cancelling timers

setTimeout/setInterval return a timer ID immediately; the callback runs later, asynchronously. clearTimeout()/clearInterval() only work if called with the exact ID returned by the matching setTimeout()/setInterval() call. Calling clearTimeout() on an ID that has already fired (or never existed) is harmless — it simply does nothing.

// Run once, after 2 seconds
const timeoutId = setTimeout(() => {
  console.log('2 seconds have passed');
}, 2000);

// Cancel it before it fires
clearTimeout(timeoutId);

// Run every 1 second, until stopped
const intervalId = setInterval(() => {
  console.log('tick');
}, 1000);

// Stop the repeating timer
clearInterval(intervalId);

Code Examples

This demonstrates that setTimeout() is non-blocking: 'Start' and 'End' both log immediately, one right after the other, and only after roughly 1.5 seconds does the scheduled callback finally run and log its message. Expected result: Start End (runs immediately, before the timeout) This runs after 1.5 seconds setTimeout(() => {... }, 1500);

setInterval() calls the callback roughly every 1000ms. Each tick, it logs the remaining count and decrements it. Once the countdown reaches below zero, the code calls clearInterval() with the same ID that setInterval() returned, which stops future ticks — otherwise the interval would keep running forever. Expected result: 3 2 1 0 Liftoff!

Common mistake: Forgetting to call clearInterval() when the repeating work is done creates a timer that runs forever in the background, silently wasting resources — this is a common source of memory/performance leaks.

Debouncing is a classic real-world pattern built entirely from setTimeout and clearTimeout. Each call to debouncedSearch() cancels any previously scheduled call and schedules a new one. If calls arrive faster than the delay, earlier ones are cancelled before they ever fire, so search() only actually runs once the user pauses typing for 300ms. Expected result: Searching for: java Debouncing is widely used for search-as-you-type boxes, window resize handlers, and auto-save features, to avoid running expensive work on every single keystroke or event.

clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay);

console.log('Start');

setTimeout(() => {
  console.log('This runs after 1.5 seconds');
}, 1500);

console.log('End (runs immediately, before the timeout)');
function startCountdown(seconds) {
  let remaining = seconds;

  const intervalId = setInterval(() => {
    console.log(remaining);
    remaining--;

    if (remaining < 0) {
      clearInterval(intervalId);
      console.log('Liftoff!');
    }
  }, 1000);
}

startCountdown(3);
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn(...args), delay);
  };
}

function search(query) {
  console.log('Searching for:', query);
}

const debouncedSearch = debounce(search, 300);

// Simulating rapid typing — only the LAST call actually runs search()
debouncedSearch('j');
debouncedSearch('ja');
debouncedSearch('jav');
debouncedSearch('java');
// After 300ms of no further calls: 'Searching for: java'

Important Rules

setTimeout(fn, ms) schedules fn to run once, after at least ms milliseconds, without blocking subsequent code.

setInterval(fn, ms) schedules fn to run repeatedly, roughly every ms milliseconds, until cancelled.

clearTimeout(id) and clearInterval(id) cancel a pending or repeating timer using the ID returned when it was created.

A delay of 0ms still defers execution until after the current script finishes — it never runs synchronously.

Delays are minimums, not guarantees — a busy main thread, background tab throttling, or browser clamping can all push the actual firing time later.

Where this shows up

Timers power auto-advancing image carousels, 'save draft' auto-save every few seconds, session-timeout warnings, polling a server for new notifications, animation loops, and debounced or throttled event handlers for search boxes, resize events, and scroll listeners across virtually every interactive website.

setInterval(() => checkForUpdates(), 1000); // ID never saved const id = setInterval(() => checkForUpdates(), 1000); // save id, call clearInterval(id) when done

Without saving the timer ID, there is no way to ever stop the interval, causing it to run forever in the background — even after the component or feature that started it is no longer needed.

setTimeout(doSomething(), 1000); // calls doSomething immediately setTimeout(doSomething, 1000); // passes the function itself, not its result doSomething() with parentheses calls the function immediately and passes its return value to setTimeout, instead of passing the function itself to be called later. The parentheses should be omitted (or wrapped in an arrow function if arguments are needed).

[Multiple Choice] What does setTimeout(fn, 0) actually do? A. Runs fn synchronously, immediately, before any other code B. Never runs fn at all C. Queues fn to run as soon as the current script finishes and the event loop is free D. Throws an error because 0 is not a valid delay

C. Queues fn to run as soon as the current script finishes and the event loop is free — Even with a delay of 0, setTimeout always defers execution — fn is placed in the task queue and only runs after the currently executing script completes and the event loop picks it up.

[True / False] Once you call setInterval(), the only way to stop it from running forever is to close the browser tab. A. True B. False

B. False — False. Calling clearInterval() with the ID returned by setInterval() stops the repeating callback at any time — you do not need to close the tab.

Write a function pollEvery(callback, ms, maxTimes) that calls callback immediately, then again every ms milliseconds, stopping automatically (using clearInterval) after it has been called maxTimes total.

function pollEvery(callback, ms, maxTimes) { let count = 0; callback(); count++; const intervalId = setInterval(() => { callback(); count++; if (count >= maxTimes) { clearInterval(intervalId); } }, ms); } pollEvery(() => console.log('poll'), 1000, 3); // logs 'poll' 3 times total, then stops

Track a counter inside the interval callback and compare it to maxTimes to know when to clear the interval.