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:
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));Then, inside any async function:
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:
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:
import { setTimeout } from "node:timers/promises";
await setTimeout(1000); // wait 1 second
const value = await setTimeout(1000, "done"); // resolves to "done" after 1sThis 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:
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:
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 hangingWait 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:
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):
// 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 1sThis 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:
const t0 = performance.now();
await sleep(1000);
console.log(`waited ${performance.now() - t0} ms`); // e.g. 1001.3Date.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
- JavaScript Promises: the complete guide: how
async/await,.then, and the promise thesleephelper returns actually work. - Add a timeout to fetch() with AbortController: the same
AbortSignalpattern applied to cancelling a network request, which is the usual partner to a polling loop.
Sources
Authoritative references this article was fact-checked against.
- Window: setTimeout() method (MDN)developer.mozilla.org
- Node.js timers/promises (official docs)nodejs.org
- Performance: now() method (MDN)developer.mozilla.org
- Heavy throttling of chained JS timers beginning in Chrome 88 (Chrome for Developers)developer.chrome.com





