TechEarl

How to sleep(), wait, and poll in JavaScript

JavaScript has no blocking sleep(). Here is the one-liner that actually works (await a Promise around setTimeout), Node's native timers/promises, a cancellable polling helper, and the setTimeout 4ms-clamp and Date.now vs performance.now gotchas.

Ishan Karunaratne⏱️ 9 min readUpdated
Share thisCopied
JavaScript has no blocking sleep. Await a Promise around setTimeout, use Node's native timers/promises, poll for a condition with AbortSignal, and know the setTimeout 4ms clamp and Date.now vs performance.now difference.

JavaScript has no blocking sleep(). The language is single-threaded, so a function that froze the thread for two seconds would freeze the whole page (or the whole Node event loop) along with it: no clicks, no rendering, no timers, nothing. Instead of blocking, you schedule a timer and await it. This one line is the answer almost everyone is looking for:

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

Then, inside any async function:

javascript
async function te_run() {
  console.log("start");
  await sleep(2000); // pauses THIS function for 2s, frees the thread meanwhile
  console.log("2 seconds later");
}

await sleep(2000) suspends only the current async function. The event loop keeps running, the UI stays responsive, other timers and clicks still fire. That is the whole trick: you are not stopping time, you are yielding until a timer fires.

A few things worth knowing up front. The helper does not need to be wrapped in an async function itself, because it already returns a promise. People call it sleep, delay, or wait interchangeably; name it whatever your team searches for. And you can only await it inside an async function (or at the top level of an ES module, where top-level await is allowed since ES2022).

The blocking version you should not write

For completeness, here is the thing people sometimes reach for. Do not ship it:

javascript
function te_busy_sleep(ms) {
  const end = Date.now() + ms;
  while (Date.now() < end) {} // burns 100% CPU, blocks everything
}

This pegs a core and freezes the page or the server for the full duration. There is exactly one place real blocking is legitimate, and it is not the main thread, see the Atomics.wait caveat below.

Node.js: a native sleep, no wrapper needed

In Node you do not have to hand-roll the promise wrapper at all. The node:timers/promises module ships a promise-returning setTimeout:

javascript
import { setTimeout } from "node:timers/promises";

await setTimeout(1000);              // wait 1 second
const value = await setTimeout(1000, "done"); // resolves to "done" after 1s

This has been stable since Node 16 (it landed in 15.0) and is the cleanest way to pause in a Node script. Note the import shadows the global setTimeout, so alias it if you still need the callback form in the same file:

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

The promise version also takes an AbortSignal, which matters the moment you need to cancel a pending wait:

javascript
import { setTimeout } from "node:timers/promises";

const ac = new AbortController();
setTimeout(60_000, undefined, { signal: ac.signal })
  .then(() => console.log("woke up"))
  .catch(err => {
    if (err.name === "AbortError") console.log("cancelled");
  });

ac.abort(); // the pending sleep rejects with AbortError instead of hanging

Wait for a condition: a cancellable polling helper

Sleeping a fixed time is the easy case. More often you want to wait until something becomes true: an element appears, a flag flips, a job finishes. There is no event to await, so you poll: check, wait a beat, check again, with a hard timeout so you never spin forever. Here is a dependency-free teWaitFor that uses AbortSignal.timeout() for the deadline:

javascript
async function teWaitFor(condition, { interval = 100, timeout = 5000 } = {}) {
  const deadline = AbortSignal.timeout(timeout);
  while (!condition()) {
    if (deadline.aborted) {
      throw new Error(`teWaitFor: condition not met within ${timeout}ms`);
    }
    await new Promise(resolve => setTimeout(resolve, interval));
  }
}

// wait until a global is set, polling every 200ms, giving up after 10s
await teWaitFor(() => window.myWidget?.ready, { interval: 200, timeout: 10_000 });

AbortSignal.timeout(ms) (Chrome 103 and Node 17.3 in 2022, Safari in 2024, so it is Baseline-wide now) gives you a signal that aborts itself after the timeout, so you do not have to manage a stray timer. Polling is a fallback: if the thing you are waiting on can emit an event or hand you a promise, await that instead. Polling is for the cases where it genuinely cannot, like a third-party script that sets a global with no callback.

If your condition is itself async (an HTTP probe, a DB ping), make condition an async function and await condition() in the loop. For network probes specifically, give each attempt its own timeout; see adding a timeout to fetch with AbortController.

The one place you can truly block: Atomics.wait

There is exactly one way to block in JavaScript, and it is deliberately walled off from the main thread. Atomics.wait() blocks the calling thread until a shared-memory value changes, but the spec forbids calling it on the main thread of a page; it only works inside a Web Worker (or a Node worker thread):

javascript
// inside a Worker only, throws on the main thread
const buf = new Int32Array(new SharedArrayBuffer(4));
Atomics.wait(buf, 0, 0, 1000); // truly blocks this worker for up to 1s

This is for thread-coordination primitives, not for "pause my code." Unless you are building something around SharedArrayBuffer, the promise-based sleep is what you want.

setTimeout is not precise: the 4ms clamp and tab throttling

await sleep(ms) schedules a timer, and timers are best-effort, not exact. Two browser behaviors trip people up:

The 4ms minimum clamp. Per the HTML spec, once you nest timers five deep (a setTimeout whose callback sets another setTimeout, five levels in), the browser clamps any delay below 4ms up to 4ms. So setTimeout(fn, 0) in a deep chain is really setTimeout(fn, 4). This is why setTimeout(fn, 0) is a poor tool for yielding to the event loop; queueMicrotask or a MessageChannel is faster if you need a true zero-delay yield.

Background-tab throttling. When a tab is in the background, browsers throttle its timers hard to save battery. In a hidden tab, Chrome's standard throttling checks timers at most once per second. On top of that, Chrome 88 added intensive throttling: once a chain of timers is nested five deep, the tab has been hidden for more than five minutes, and it has been silent for at least 30 seconds, the timers are checked only once per minute. So a sleep(1000) loop you expect to tick every second drops to one tick per second the moment the tab goes to the background, and as slow as one tick per minute once those intensive-throttling conditions are all met. If precise timing matters (animation, scheduling), do not lean on setTimeout; use requestAnimationFrame for visual work, and recompute against the clock rather than counting ticks.

Measuring how long you waited: Date.now() vs performance.now()

If you are timing the wait, pick the right clock. They are not interchangeable:

javascript
const t0 = performance.now();
await sleep(1000);
console.log(`waited ${performance.now() - t0} ms`); // e.g. 1001.3

Date.now() is wall-clock time: milliseconds since the Unix epoch. It is what you want for timestamps and dates, but it can jump backward or forward when the OS clock is corrected (NTP sync, the user changes the date, daylight-saving). Subtracting two Date.now() readings can therefore give you a negative or wildly wrong duration.

performance.now() is monotonic: a high-resolution clock that only ever moves forward and is immune to clock adjustments. It is measured in fractional milliseconds from a fixed origin, so it is the correct tool for measuring elapsed time, benchmarking, and computing how long an operation actually took. Rule of thumb: Date.now() for when, performance.now() for how long. In Node, performance.now() is available globally and behaves the same way.

See also

Sources

Authoritative references this article was fact-checked against.

TagsjavascriptsleepsetTimeoutasync-awaitpromisespollingnode.jsperformance.now

Found this useful? Pass it on.

Copied

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

How to Merge Two Arrays in JavaScript

Merge two arrays in JavaScript three ways: copy-merge with spread [...a, ...b], merge in place with a.push(...b), or copy with a.concat(b). Which to pick by mutation, memory, and the spread arg-count limit that bites on very large arrays.

How async Functions Really Work in JavaScript

What an async function actually is under the hood: it desugars to a generator plus a built-in runner. Plus async generators with for await...of, the AsyncFunction constructor, and why detecting async-ness is a trap.

How to Set Up Git for a New Project

Set up Git for a new project the right way: git init, a starter .gitignore, your first commit, creating the remote, pushing, and turning on branch protection.