JavaScript has no built-in sleep() function. That's not an oversight — it's a consequence of the language's single-threaded event loop, where blocking the main thread for one second locks every animation, network response, and click handler in the page. This guide covers the canonical sleep pattern, every variant you actually need (cancellable, retryable, tested), the exact gotchas that bite developers in production, and the one fake-blocking pattern you should know exists and never ship.
The one-line answer
The canonical sleep in modern JavaScript:
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// Use it
await sleep(2000); // pause for 2 seconds inside an async functionThat's the whole pattern. The rest of this article is when to use variants of it, the bugs you'll hit if you misuse it, and what's happening underneath.
Why JavaScript doesn't have sleep()
JavaScript runs on a single thread. There is one call stack, one event loop, one render queue. A blocking sleep(1000) would freeze:
- UI updates and CSS transitions
- Click, scroll, and keyboard handlers
- The microtask queue (resolved Promises)
- Network response handlers
requestAnimationFramecallbacks
Languages with native sleep() (Python, Java, C#, Go) all run threads or coroutines that the runtime can park while the rest of the program continues. JavaScript chose a different concurrency model: cooperative async with non-blocking I/O. So instead of pausing a thread, you pause a Promise — the thread keeps running everything else.
The canonical sleep pattern, explained
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));What's happening:
new Promise(...)creates a Promise that hasn't resolved yet.setTimeout(resolve, ms)schedules the Promise'sresolvefunction to run aftermsmilliseconds.- When you
await sleep(ms), the surrounding async function suspends until the Promise resolves; the event loop is free to do other work in the meantime.
This is the right answer in browsers, Node, Deno, Bun, and Cloudflare Workers.
Use it inside an async function
async function fadeIn(element) {
for (let opacity = 0; opacity <= 1; opacity += 0.1) {
element.style.opacity = opacity;
await sleep(50);
}
}Use it at top level if you're in an ES module
// In a .mjs file, an <script type="module">, or any modern bundler
await sleep(1000);
console.log("done");Top-level await works in ES modules. It does not work in classic scripts, CommonJS, or the body of a non-async function.
Sleep in loops — the most common bug
This doesn't work the way it looks like it should:
// Wrong — all timeouts fire after ~1 second, in parallel
[1, 2, 3, 4, 5].forEach(async (n) => {
await sleep(1000);
console.log(n);
});Array.prototype.forEach ignores Promise return values. Each iteration's async callback fires immediately, the awaits run independently, and all five logs print at roughly the same time.
The fix is a regular for loop, where each iteration genuinely waits for the previous one:
// Correct — one log per second, in order
for (const n of [1, 2, 3, 4, 5]) {
await sleep(1000);
console.log(n);
}If you actually want them to fire in parallel — say, for rate-limited API requests — use Promise.all with a deliberate stagger:
await Promise.all(
ids.map(async (id, i) => {
await sleep(i * 200); // stagger by 200ms
return fetchOne(id);
})
);Cancellable sleep with AbortSignal
The naive sleep can't be interrupted — you wait the full duration whether you need to or not. For UI work, network retries, and tests, you want a cancellable version. Use AbortSignal:
function sleep(ms, signal) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const timer = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(signal.reason);
}, { once: true });
});
}
// Usage
const ctrl = new AbortController();
sleep(5000, ctrl.signal).catch(err => console.log("aborted:", err.message));
// Anywhere else — cancels the sleep early
ctrl.abort(new DOMException("user cancelled", "AbortError"));This pattern composes cleanly with fetch's AbortSignal support and React effect cleanup.
Node.js: use the built-in
Since Node 16 (and stable since Node 18), Node ships a Promise-based setTimeout in the node:timers/promises module. It's the recommended sleep in Node code:
import { setTimeout as sleep } from "node:timers/promises";
await sleep(1000);
// With a return value
const result = await sleep(1000, "done");
console.log(result); // "done"
// With AbortSignal
const ctrl = new AbortController();
sleep(5000, undefined, { signal: ctrl.signal })
.catch(err => console.log("cancelled"));
ctrl.abort();It's marginally faster than the userland Promise wrapper and supports unref-by-default and AbortSignal natively. Use it when you're targeting Node only.
TypeScript signature
const sleep = (ms: number, signal?: AbortSignal): Promise<void> =>
new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const timer = setTimeout(resolve, ms);
signal?.addEventListener("abort", () => {
clearTimeout(timer);
reject(signal.reason);
}, { once: true });
});A real-world use case: retry with exponential backoff
This is what most production "sleep" usage actually looks like — sleeping between retries when a remote service is rate-limited or temporarily failing:
async function withRetry(fn, { maxAttempts = 5, baseMs = 200 } = {}) {
let lastError;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
lastError = err;
if (attempt < maxAttempts - 1) {
const delay = baseMs * 2 ** attempt + Math.random() * baseMs;
await sleep(delay); // exponential + jitter
}
}
}
throw lastError;
}
const data = await withRetry(() => fetch("/api/flaky").then(r => r.json()));The jitter (random component) prevents thundering herds when many clients retry in lockstep.
Testing code that uses sleep
Real timers in tests make the suite slow and flaky. Every modern test runner has fake timers:
Jest / Vitest
import { describe, it, expect, vi } from "vitest";
it("waits the right amount", async () => {
vi.useFakeTimers();
const promise = sleep(5000).then(() => "done");
vi.advanceTimersByTime(5000);
expect(await promise).toBe("done");
vi.useRealTimers();
});The same API works in Jest with jest.useFakeTimers() / jest.advanceTimersByTime().
Node's built-in test runner
import { test } from "node:test";
import { setTimeout as sleep } from "node:timers/promises";
test("uses fake timers", async (t) => {
t.mock.timers.enable({ apis: ["setTimeout"] });
const promise = sleep(5000);
t.mock.timers.tick(5000);
await promise;
});Sleep vs setTimeout vs setInterval
| Use case | Use |
|---|---|
| Pause inside an async function | await sleep(ms) |
| Run something once after a delay | setTimeout(fn, ms) |
| Run something repeatedly on a fixed cadence | setInterval(fn, ms) |
| Run something every animation frame | requestAnimationFrame(fn) |
| Run something when the browser is idle | requestIdleCallback(fn) |
| Defer a callback to the next microtask | queueMicrotask(fn) |
setInterval looks like a recurring sleep, but it has a foot-gun: if a tick takes longer than the interval, intervals queue up. For periodic work, prefer a recursive setTimeout or a while (true) { await sleep(...); ... } loop with a kill switch.
Browser quirks: minimum delays and tab throttling
The number you pass to setTimeout is a minimum, not a guarantee. Browsers enforce several lower bounds:
- Nested timeouts: after 5 nested
setTimeoutcalls, browsers clamp the minimum delay to 4 ms (the HTML spec). - Inactive tabs: Chrome and Firefox throttle background tabs to roughly 1 second minimum and apply additional intensive throttling after a few minutes of inactivity.
- Page load: Firefox defers some
setTimeoutcalls during the page-load critical path. - Maximum delay: the delay is internally a 32-bit signed integer (max ≈ 2,147,483,647 ms ≈ 24.8 days). In Node, larger values run immediately; in browsers they wrap around. Don't pass huge numbers.
For animations and time-sensitive work, never rely on setTimeout precision. Use requestAnimationFrame or performance.now()-based diff calculations instead.
The blocking "fake sleep" — exists, never ship it
You'll occasionally see a synchronous busy-wait pattern presented as "sleep":
function badSleep(ms) {
const end = Date.now() + ms;
while (Date.now() < end) {} // freezes the thread
}This blocks the entire JavaScript thread. In a browser, the page becomes unresponsive — no clicks, no scrolling, no rendering, no network handlers. In Node, no other Promise, timer, or I/O callback runs. It is genuinely useful in exactly one scenario: stalling a worker thread inside a Web Worker for a benchmark or a deliberate stress test, where you have no other work to do. Anywhere else, it's a bug.
If you ever need to delay between synchronous operations in code that cannot be made async — say, a third-party API that demands sync — refactor the calling code instead. There is no good way around it.
Sleep in popular frameworks
React
useEffect(() => {
const ctrl = new AbortController();
(async () => {
try {
await sleep(2000, ctrl.signal);
setStatus("done");
} catch (err) {
if (err.name !== "AbortError") throw err;
}
})();
return () => ctrl.abort();
}, []);Vue 3 (Composition API)
onMounted(async () => {
await sleep(500);
state.ready = true;
});Express middleware (Node)
import { setTimeout as sleep } from "node:timers/promises";
app.get("/slow", async (req, res) => {
await sleep(1000);
res.json({ ok: true });
});Sleep in JavaScript vs other languages
| Language | Sleep call | Blocking? |
|---|---|---|
| JavaScript (browser/Node) | await new Promise(r => setTimeout(r, ms)) | No (cooperative) |
| Python | time.sleep(seconds) / await asyncio.sleep(seconds) | Yes / No |
| Java | Thread.sleep(ms) | Yes |
| C# | Thread.Sleep(ms) / await Task.Delay(ms) | Yes / No |
| Go | time.Sleep(time.Second) | Goroutine only |
| Rust | std::thread::sleep / tokio::time::sleep(...).await | Yes / No |
| Bash | sleep N | Yes |
Frequently asked questions
How do I sleep for 1 second in JavaScript?
await new Promise(resolve => setTimeout(resolve, 1000)), inside an async function or at the top level of an ES module.
Why doesn't setTimeout alone work like sleep?
setTimeout schedules a callback; it does not pause execution. The line after setTimeout runs immediately. Wrap it in a Promise and await the Promise to actually pause.
Can I sleep synchronously without freezing the page?
No — those two requirements contradict each other in JavaScript. Synchronous code blocks the single thread by definition. The closest you can get to "synchronous-looking" code that doesn't block is async/await with a Promise-based sleep.
Does await sleep(0) do anything?
Yes — it yields to the event loop. Other queued tasks (microtasks, pending I/O callbacks) get a chance to run before your code continues. Useful for breaking up long synchronous loops without blocking the UI.
What's the maximum delay setTimeout accepts?
2,147,483,647 ms (about 24.8 days). Larger values overflow — Node runs the callback immediately, browsers wrap around. Don't pass huge numbers.
Is setImmediate the same as sleep(0)?
Close but not identical. setImmediate(fn) (Node-only) runs fn in the next iteration of the event loop, after I/O callbacks. setTimeout(fn, 0) typically runs slightly later. queueMicrotask(fn) runs sooner — at the end of the current task.
How do I sleep until a specific time?
Compute the difference and sleep for that many milliseconds:
const target = new Date("2026-01-01T00:00:00Z");
await sleep(target - Date.now());For long delays (hours+), prefer scheduling a job rather than holding a Promise open — the timer will be reset if the user reloads the page.
How do I sleep in a service worker?
Don't. Service workers can be terminated at any time. Use the Periodic Background Sync API or schedule work via setTimeout only for very short delays.
Why is my await sleep(...) running too late?
Most likely your tab is backgrounded (Chrome and Firefox throttle inactive tabs to ~1s minimum), or you're past the 5-nested-timeout threshold (4ms minimum applies). If you need precise timing, use requestAnimationFrame with elapsed-time math.
Can I cancel an in-progress sleep?
Only if you wired up AbortSignal support. The naive sleep(ms) has no cancel mechanism — see the cancellable variant above.
Hire JavaScript engineers who debug the event loop, not fight it
Async/await, Promise scheduling, the difference between microtasks and macrotasks, browser throttling rules — modern frontend and Node work depends on developers who think in the event loop, not against it.
Codersera matches you with vetted remote JavaScript and TypeScript engineers who have shipped production code in React, Node, Deno, and Bun. Each developer is technically interviewed, reference-checked, and ready to extend your engineering team — with a risk-free trial period to confirm the technical fit.