Devpuff
Pricing
Log InStart Free
  1. Home
  2. Blog
  3. How to sleep or wait in JavaScript
Learn

How to sleep or wait in JavaScript

JavaScript has no sleep function. The three-line version everyone writes does not block the page, and a busy-wait loop does.

By Max Arthur
Co-Founder & Content Marketer·October 1, 2026·9 Min read
White text reading 'How to sleep or wait in JavaScript' on a dark purple dotted background

JavaScript has no built-in sleep. This is the standard replacement:

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

await sleep(1000);

Three lines, and it works because setTimeout resolves the promise after the delay and await waits for that promise.

Measured in Node v24.14.1, await sleep(200) took 211.5ms. Close to the request, slightly over, for the reasons in setTimeout and setInterval.

It does not actually block

This is the part worth understanding, because the name is misleading.

const slow = (async () => {
  await sleep(150);
  order.push("slow done");
})();
order.push("main thread kept going");
await slow;
["main thread kept going", "slow done"]

The line after the sleep ran first. await pauses only the async function it is inside; everything else on the page carries on. Clicks are handled, animations run, other requests resolve.

That is the opposite of sleep in most languages, where the thread genuinely stops. In JavaScript there is one thread shared with the entire page, so a real blocking sleep would freeze the interface, and the language does not provide one for that reason.

await sleep(...) means "continue this function later", not "stop everything".

The busy-wait, and why not to

You can block the thread, and it is worth seeing what it costs:

const spin = performance.now() + 100;
while (performance.now() < spin);

Measured, that blocked for 100ms exactly, and during it nothing else could run at all. No rendering, no clicks, no timers.

It is precise, which is its only advantage. For 100ms a user sees a stutter; for anything longer the browser may offer to kill the page. There is essentially no situation in front-end code where this is the right tool.

Sequential and parallel

The most valuable thing to know about sleep is what happens when you use several.

await Promise.all([sleep(100), sleep(100), sleep(100)]);

Measured: 115ms. All three ran at once.

for (let i = 0; i < 3; i++) await sleep(100);

Measured: 317ms. Each waited for the last.

Same three sleeps, three times the duration. This is the single most common performance mistake in async code, and it usually appears with real work rather than sleeps:

// 3 seconds if each request takes 1
for (const id of ids) results.push(await fetchUser(id));

// 1 second
const results = await Promise.all(ids.map(fetchUser));

await inside a loop is sequential by definition. When the operations do not depend on each other, Promise.all is the fix, and the difference is the number of operations multiplied.

Sequential is sometimes what you want: rate limiting, or when each step needs the previous result. The point is to choose rather than to discover it later.

Where sleep is genuinely useful

Retrying with a backoff. The clearest legitimate use:

async function retry(fn, attempts = 3) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (i === attempts - 1) throw err;
      await sleep(2 ** i * 1000);   // 1s, 2s, 4s
    }
  }
}

Doubling the wait each time is exponential backoff, and it is the polite way to retry a failing service rather than hammering it.

Rate limiting. Pausing between requests to stay under an API's limit.

Demonstrations and tests. Simulating a slow network to check a loading state actually appears.

Polling with a guaranteed gap:

while (!done) {
  await check();
  await sleep(5000);
}

Because the sleep comes after the work, requests never overlap, which is the same advantage recursive setTimeout has over setInterval.

Where it is the wrong tool

Waiting for something to be ready. Sleeping two seconds and hoping an element exists is guessing. It is too slow when the thing is ready sooner and broken when it is not.

// guessing
await sleep(2000);
doTheThing();

// waiting for the actual event
await new Promise((resolve) => el.addEventListener("load", resolve, { once: true }));
doTheThing();

The same applies to MutationObserver for DOM changes, and to simply awaiting the promise that represents the work. Sleeping for a fixed time to synchronise is the most common misuse, and it produces tests that pass on a fast machine and fail in CI.

Delaying a UI transition. CSS does that with transition-delay, without JavaScript.

Throttling rapid events. Debounce and throttle handle scroll, resize and typing better than a sleep, because they cancel pending work rather than queueing more.

Cancelling a sleep

The basic version cannot be cancelled, which matters when a component unmounts partway through a retry loop. An AbortSignal fixes it:

const sleep = (ms, signal) =>
  new Promise((resolve, reject) => {
    const id = setTimeout(resolve, ms);
    signal?.addEventListener("abort", () => {
      clearTimeout(id);
      reject(new DOMException("Aborted", "AbortError"));
    }, { once: true });
  });

Now an aborted controller stops the wait and rejects, so the code after it does not run against a component that is gone. The clearTimeout matters too: without it, the timer keeps a reference alive until it fires.

Node has this built in as timers/promises:

import { setTimeout as sleep } from "node:timers/promises";
await sleep(1000);

Worth using in Node rather than writing your own, since it supports AbortSignal already.

Limiting concurrency

Promise.all runs everything at once, which is right for three requests and wrong for three hundred. Firing hundreds of requests simultaneously will hit rate limits, exhaust connection pools, or simply be refused.

The middle ground is a pool that keeps a fixed number in flight:

async function pool(items, limit, worker) {
  const results = [];
  const running = new Set();

  for (const item of items) {
    const p = worker(item).then((r) => {
      running.delete(p);
      return r;
    });
    running.add(p);
    results.push(p);
    if (running.size >= limit) await Promise.race(running);
  }
  return Promise.all(results);
}

await pool(ids, 5, fetchUser);

Promise.race resolves as soon as any one finishes, which is what frees a slot. Five requests stay in flight, and a sixth starts only when one completes.

This is the shape worth reaching for whenever the list length is not known in advance. Sequential is too slow, all-at-once is antisocial, and a pool of five or ten is almost always the right answer.

Testing code that waits

Real sleeps make tests slow and flaky. A suite with twenty retry tests at two seconds each takes forty seconds and occasionally fails on a loaded CI machine.

Use fake timers instead:

jest.useFakeTimers();
const promise = retryingFunction();
await jest.advanceTimersByTimeAsync(5000);
await promise;

The test controls the clock, so five seconds of waiting takes no real time and the timing is exact rather than approximate. Vitest has the same API under vi.

The general rule for tests: never wait for a duration, wait for a condition. A test that sleeps 500ms and then asserts is guessing, and it will fail on the machine that is slower than yours. Awaiting the actual promise, or a testing library's waitFor, is both faster and reliable.

Sleep in Node versus the browser

Node has a promise-based timer built in, which is the one to use there:

import { setTimeout as sleep } from "node:timers/promises";
await sleep(1000);

It supports AbortSignal already, so the cancellable version above is not needed.

Node also has a genuinely blocking option, which the browser does not:

Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000);

That blocks the thread for real. It throws on the main thread in browsers, and in Node it is legal but almost always wrong, because it stops the event loop and nothing else can run. Its narrow use is inside a worker thread that has nothing else to do.

Adding a timeout to something else

The related pattern, and a genuinely useful one: not "wait this long" but "give up after this long".

const timeout = (ms) =>
  new Promise((_, reject) =>
    setTimeout(() => reject(new Error("timed out")), ms),
  );

const result = await Promise.race([fetchData(), timeout(5000)]);

Promise.race settles with whichever finishes first, so a slow request loses to the timer and rejects. It is the same three-line building block as sleep, with reject instead of resolve.

One caveat: racing does not cancel the losing promise. The request keeps running and its result is discarded. For fetch specifically, AbortSignal.timeout does the job properly by cancelling the request itself:

await fetch(url, { signal: AbortSignal.timeout(5000) });

That is the better tool where it applies, and the race is the general version for anything without cancellation support.

Three common mistakes

Expecting it to block. It pauses one async function. Everything else keeps running, which is usually what you want and never what the word "sleep" suggests.

Awaiting in a loop when the work is independent. Measured, three 100ms sleeps took 317ms sequentially and 115ms with Promise.all.

Sleeping to wait for something. Await the actual event or promise. A fixed delay is a guess that fails on a slow machine and wastes time on a fast one.

Sleep inside a React component

The pattern is common and the mistake is consistent:

useEffect(() => {
  let cancelled = false;

  (async () => {
    await sleep(2000);
    if (!cancelled) setShowHint(true);
  })();

  return () => { cancelled = true; };
}, []);

The cancelled flag is the point. Without it, a component that unmounts during the sleep still calls setShowHint afterwards, on a component that no longer exists. React tolerates this more gracefully than it used to and it remains a leak: the closure keeps the component's scope alive for the duration.

For this particular case a plain timer is simpler and cancels properly on its own:

useEffect(() => {
  const id = setTimeout(() => setShowHint(true), 2000);
  return () => clearTimeout(id);
}, []);

The rule of thumb: use sleep when you need to pause inside a longer async sequence, and a plain setTimeout when you just need one thing to happen later. The second cancels cleanly with no flag.

What await actually does here

Worth being precise, because "pause" is a loose word for it.

await splits the function at that point. Everything before it runs now; everything after is registered as a continuation and runs when the promise resolves. In between, control returns to the event loop and the rest of the page proceeds.

That is why the ordering measurement above came out as it did, and why sleep cannot block: there is no mechanism by which it could, because the function is not running during the wait. It has yielded.

The practical consequence is that state can change while you are sleeping. Anything read after an await may be stale, and in a component that includes props and state captured by the closure. Re-reading from a ref, or checking a cancellation flag, is how you handle it.

Quick reference

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

await sleep(1000);                          // pause this function
await Promise.all([a(), b(), c()]);         // run together
for (const x of xs) await work(x);          // run in sequence, deliberately
await sleep(2 ** attempt * 1000);           // exponential backoff

import { setTimeout as sleep } from "node:timers/promises";   // Node built-in

Want to watch the parallel and sequential versions race? Start with the JavaScript track.

Keep reading

More from the blog

White text reading 'How range() works in Python' on a dark purple dotted background
October 12, 2026·8 min readLearn

range() in Python: start, stop, step, and why it is lazy

range(1000000) uses 24 bytes. A list of a thousand numbers uses 4,028. Here is what range actually is, measured.

Read more
White text reading 'How to convert a string to a number in JavaScript' on a dark purple dotted background
October 3, 2026·9 min readLearn

How to convert a string to a number in JavaScript

Converting a string to a number in JavaScript: an empty string is 0 with Number and NaN with parseInt. What four methods do to awkward input.

Read more
{ }
✦

Ready to write some code?

Put this into practice - start your first free lesson. No setup, no credit card.

Start learning free
Devpuff

Learn to code by doing. One tiny, playful lesson at a time.

Learn
ProgramsCoursesPricing
Company
AboutBlogResourcesAffiliates
Support
Help CenterContactStatus
Programs
Frontend DevelopmentFull-Stack DevelopmentReact DeveloperPython Developer
Courses
Advanced ReactAsync JavaScriptAsync PythonCSS BasicsCSS LayoutDSA Basics
Learn to Code
Learn JavaScriptLearn PythonLearn SQLLearn HTMLLearn CSSLearn ReactBrowse All Topics
Platform Comparisons
Devpuff vs CodecademyDevpuff vs Mimo
© 2026 Devpuff. All rights reserved.Privacy PolicyTerms and ConditionsCookies PolicyRefund Policy
Devpuff vs Sololearn
Devpuff vs freeCodeCamp
Read All Comparisons