JavaScript has no built-in sleep function, and that is deliberate: the language runs on a single thread with an event loop, so genuinely blocking it would freeze the entire page or, in Node, stop the process from handling anything else. The replacement is a promise that resolves after a timeout, awaited.
That three-line helper is the answer to the question as asked. The more useful discussion is when a delay is the right tool at all, because a large share of the code that reaches for sleep is working around a race condition rather than solving one.
Table of contents
- The helper
- What actually blocks, and why not to
- Where a delay is genuinely correct
- Where a delay is hiding a bug
- Sleeping inside loops
- Timers are not precise
- How this fits the rest of the stack
- FAQ
The helper
This is the version to use:
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
async function run() {
console.log('start');
await sleep(1000);
console.log('one second later');
}
It works because await yields control back to the event loop rather than holding it. Other timers fire, events are handled, and the browser keeps rendering. The function resumes when the timeout completes.
In Node there is a built-in version, which is worth using since it also supports cancellation:
import { setTimeout as sleep } from 'node:timers/promises';
await sleep(1000);
One important limitation: this only pauses the async function it is written in. Code outside that function keeps running. It is not a global pause and cannot be made into one.
What actually blocks, and why not to
A genuinely blocking sleep can be written, and it is worth seeing precisely so you recognise it as a mistake:
// Do not do this.
function blockingSleep(ms) {
const end = Date.now() + ms;
while (Date.now() < end) { /* burn CPU */ }
}
This pins a CPU core at 100% and blocks the single thread completely. In a browser the page stops responding entirely — no clicks, no scrolling, no rendering. In Node the process serves no requests for the duration.
There is one legitimate blocking primitive, Atomics.wait, and it only works inside a worker thread — the specification forbids it on the main thread precisely because of the above. If you are in a worker and genuinely need to block, that is the tool. If you are not, there is no correct blocking sleep.
Where a delay is genuinely correct
Three cases where waiting is the right behaviour rather than a workaround:
- Retry with backoff. A failed request should not be retried immediately. Waiting, with the interval growing each attempt, is the correct pattern.
- Rate limiting. An API allows ten requests a second and you have a thousand to make. Pacing them is required.
- Animation and deliberate timing. A message that shows for two seconds before dismissing. The delay is the feature.
The retry case is worth writing properly, because the naive version causes problems at scale:
async function withRetry(fn, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
if (i === attempts - 1) throw err;
const backoff = 2 ** i * 100;
const jitter = Math.random() * 100;
await sleep(backoff + jitter);
}
}
}
The jitter matters. Without it, every client that failed at the same moment retries at the same moment, and the service that was struggling gets a synchronised burst. Randomising the delay spreads them out. This is a real production failure mode and the fix is one line.
Where a delay is hiding a bug
The pattern to be suspicious of is waiting a fixed time for something to become ready.
// Fragile: hopes 500ms is enough
renderChart();
await sleep(500);
readChartDimensions();
This works on your machine and fails on a slower one, under load, or on a bad network. It is not a fix; it is a bet on timing that you will lose intermittently, in production, with no clear error.
The correct version waits for the actual condition. If the operation returns a promise, await it. If it fires an event, listen for it. If it is a DOM change, use MutationObserver or ResizeObserver. If you genuinely have no signal, poll for the condition with a timeout rather than sleeping a fixed duration:
async function waitFor(condition, { timeout = 5000, interval = 50 } = {}) {
const deadline = Date.now() + timeout;
while (Date.now() < deadline) {
if (condition()) return true;
await sleep(interval);
}
throw new Error('Timed out waiting for condition');
}
This succeeds as soon as the condition holds rather than always waiting the full duration, and it fails with a clear error instead of silently continuing with the wrong state.
The tell for this class of bug: a sleep whose duration was chosen by trying numbers until the flakiness stopped. If nobody can say why it is 500 and not 300, it is a bet.
Sleeping inside loops
A common need is to process items with a delay between them. The straightforward version works:
for (const item of items) {
await process(item);
await sleep(200);
}
The mistake to avoid is expecting this inside forEach or map:
// Broken: forEach ignores the returned promise
items.forEach(async item => {
await process(item);
await sleep(200);
});
console.log('done'); // runs immediately, nothing is done
forEach does not await the async callback. All iterations start at once and the loop returns immediately, so the log fires before any work completes. Use a for…of loop when order and sequencing matter.
When you want concurrency but bounded, sleeping between items is the wrong tool entirely — you want a concurrency limit, processing several at a time with a cap, rather than one at a time with gaps.
Timers are not precise
setTimeout guarantees a minimum delay, not an exact one. The callback runs after the delay has elapsed and the event loop is free. A busy thread pushes it later, sometimes considerably.
Two behaviours worth knowing:
- Background tab throttling. Browsers clamp timers in inactive tabs, often to once per minute. A polling loop in a background tab runs far less often than written, which is usually desirable and occasionally surprising.
- Nested timeout clamping. After several levels of nesting, browsers enforce a 4ms minimum, so sleep(0) is not zero.
The consequence for anything timing-sensitive: measure elapsed time rather than counting iterations. Code that assumes ten iterations of sleep(100) equals one second will drift, and the drift accumulates.
How this fits the rest of the stack
The distinction running through all of this — waiting for a condition against waiting a duration — is the same one that separates a reliable deploy from a flaky one. A health check that confirms the service is actually answering is a condition; a fixed pause before routing traffic is a bet. Services deployed on RunxBuild expose runtime logs next to the deploy that produced them, which is what turns an intermittent timing failure into something you can correlate rather than guess at. Node services on RunxBuild covers that path, and the RunxBuild hosting calculator itemises what the service and its database cost together.
Useful related references:
- Node.js Express vs Fastify vs Koa vs Hapi: The Performance, the Plugin Story, the Async Story, and the Right Choice for 2026
- Node.js 20.11.1: The Quiet Patch Every LTS Team Should Actually Care About
- SyntaxError Unexpected Token: The Five Things It Actually Means in JavaScript and TypeScript
- Services on RunxBuild
FAQ
Why does JavaScript have no sleep function?
Because it runs on a single thread with an event loop. A blocking sleep would freeze the entire page or stop a Node process from handling anything else. The async promise-and-setTimeout version yields control instead of holding it.
How do I write a sleep function in JavaScript?
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)), then await sleep(1000) inside an async function. In Node, import setTimeout from node:timers/promises for a built-in version that also supports cancellation.
Does await sleep() pause my whole program?
No. It only suspends the async function containing it. Other code, timers, and events continue running. There is no way to globally pause JavaScript execution, and that is intentional.
Why does my sleep not work inside forEach?
forEach ignores the promise returned by an async callback, so every iteration starts at once and the loop returns immediately. Use a for…of loop when you need sequential execution with awaits.
Is setTimeout accurate?
It guarantees a minimum delay, not an exact one — the callback runs once the delay elapses and the event loop is free. Background tabs are throttled heavily, and nested timeouts are clamped to about 4ms.