JavaScript Sleep: How to Pause Without Blocking
JavaScript has no general sleep statement, but a timer and a Promise provide the intentional pause most asynchronous code needs.
JavaScript sleep is a Promise-based delay that pauses only the awaiting async function, without blocking the JavaScript thread or stopping unrelated work.
The Short Answer: Create an Async Sleep Function
A reusable JavaScript sleep function resolves a Promise after a timer expires:
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
await sleep(500);
console.log('half a second passed');
half a second passed
setTimeout takes its delay in milliseconds, so 500 requests half a second and 2000 requests two seconds. The await expression pauses the code after it until the Promise settles.
If the helper is used only once, the same operation fits on one line:
await new Promise((resolve) => setTimeout(resolve, 500));
console.log('ready');
ready
For this article, sleep means an asynchronous delay of one continuation. It does not freeze the browser, stop the JavaScript thread or prevent another callback from running.
await is valid inside an async function and at the top level of an ECMAScript module. A classic browser script needs an async function:
await can live in a classic script and in a module.const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function showMessageLater() {
await sleep(300);
console.log('message shown');
}
showMessageLater();
message shown
A script loaded as a module can use top-level await directly. Async/await covers the surrounding syntax, while Scheduling: setTimeout and setInterval covers the timer underneath.
What Actually Pauses When You Await Sleep
The sleep helper joins two scheduling mechanisms. setTimeout arranges a future timer task, and the Promise gives await something to suspend on.
Start with setTimeout without await:
console.log('start');
setTimeout(() => {
console.log('timer callback');
}, 0);
console.log('end');
start
end
timer callback
A delay of 0 does not run the callback immediately. The current code finishes first, so end appears before timer callback.
An awaited sleep changes where one async function continues, but unrelated code still gets its turn. This standalone example uses the same minimal helper:
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function loadPreview() {
console.log('preview started');
await sleep(20);
console.log('preview continued');
}
loadPreview();
setTimeout(() => {
console.log('unrelated timer');
}, 0);
console.log('main code finished');
preview started
main code finished
unrelated timer
preview continued
The sequence has four parts:
loadPreview()runs until it reachesawait sleep(20).- The sleep helper creates a timer and returns a pending Promise.
- The call stack becomes available, so the main code and unrelated timer can run.
- When the sleep timer runs, it resolves the Promise. The suspended part of
loadPreviewbecomes a Promise continuation, scheduled as a microtask.
That continuation prints preview continued. Nothing held the thread for 20 milliseconds. The function stepped aside.
This distinction matters whenever several parts of a page share the main thread. Input handlers, rendering work and other callbacks still need chances to run. Event loop: microtasks and macrotasks follows those queues in detail.
Using Sleep in Functions and Loops
A sleep inside a for...of loop delays each iteration before the next one starts. The loop itself must be inside an async function or module:
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function publishStages() {
for (const stage of ['draft', 'review', 'publish']) {
await sleep(20);
console.log(stage);
}
}
await publishStages();
draft
review
publish
Each iteration waits for the preceding delay. Three delays of 20 milliseconds therefore happen in sequence.
Scheduling several equal timers together does something different:
const tasks = ['draft', 'review', 'publish'];
for (const task of tasks) {
setTimeout(() => console.log(task), 20);
}
console.log('all timers scheduled');
all timers scheduled
draft
review
publish
All three timers are registered during the same loop. The second timer does not wait for the first callback, and the third does not wait for the second. Their requested delays overlap.
forEach has the same trap with async callbacks. It calls every callback and immediately finishes iterating, without waiting for the Promises those callbacks return:
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
['draft', 'review', 'publish'].forEach(async (stage) => {
await sleep(20);
console.log(stage);
});
console.log('forEach finished');
forEach finished
draft
review
publish
Use for...of with await when order matters. Use Promise.all with map when the work should begin together and the surrounding function must wait for all of it:
const sleep = (milliseconds) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
const stages = ['draft', 'review', 'publish'];
const completed = await Promise.all(
stages.map(async (stage) => {
await sleep(20);
return `${stage} done`;
})
);
console.log(completed.join(', '));
draft done, review done, publish done
The choice is explicit: for...of creates a sequence, while Promise.all waits for a group of concurrent operations.
Make Sleep Cancellable in the Browser
A fixed delay can become irrelevant before it finishes. A user closes a panel, starts another search or leaves the page, and the old continuation no longer has useful work to do.
An AbortSignal gives the caller a standard way to cancel the wait. The canonical browser implementation handles both possible winners, the timer and cancellation:
function sleep(milliseconds, { signal } = {}) {
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(signal.reason);
return;
}
const timerId = setTimeout(() => {
signal?.removeEventListener('abort', onAbort);
resolve();
}, milliseconds);
function onAbort() {
clearTimeout(timerId);
signal.removeEventListener('abort', onAbort);
reject(signal.reason);
}
signal?.addEventListener('abort', onAbort, { once: true });
});
}
const controller = new AbortController();
setTimeout(() => {
controller.abort(new Error('delay cancelled'));
}, 20);
try {
await sleep(1000, { signal: controller.signal });
console.log('delay completed');
} catch (error) {
console.log(error.message);
}
delay cancelled
There are four pieces to keep together.
signal.abortedhandles cancellation that happened beforesleepwas called.clearTimeout(timerId)prevents the timer callback from running after cancellation wins.reject(signal.reason)preserves the caller’s reason for stopping.removeEventListenerreleases the abort listener when the timer wins normally.
The { once: true } option removes the listener after an abort event, but it cannot handle the success path because no abort event occurs there. The timer callback performs that cleanup itself.
Cancellation settles the sleep Promise early. It does not interrupt synchronous JavaScript that is already running, and it cannot undo work that another operation has completed.
The same signal can coordinate a delay with browser operations that support cancellation. Fetch: Abort develops that pattern around requests.
Use Promise-Based Timers in Node.js
Node.js has a native Promise-returning timer in node:timers/promises. Import its setTimeout under a clearer local name:
import { setTimeout as sleep } from 'node:timers/promises';
const result = await sleep(50, 'cache ready');
console.log(result);
cache ready
The first argument is the delay, the second is the value used to fulfill the Promise, and the third can contain signal and ref options:
import { setTimeout as sleep } from 'node:timers/promises';
const controller = new AbortController();
try {
await sleep(1000, undefined, {
signal: controller.signal,
ref: true,
});
} catch (error) {
if (error.name === 'AbortError') {
console.log('timer cancelled');
} else {
throw error;
}
}
controller.abort();
Here ref retains its default value of true, so the timer keeps the Node.js event loop active. Set ref: false for a timer that should not, by itself, keep the process running:
import { setTimeout as sleep } from 'node:timers/promises';
await sleep(30_000, undefined, { ref: false });
If no other active work keeps the process alive, Node.js can exit before that timer resolves. Use the portable Promise wrapper in code shared with browsers, and use node:timers/promises when Node-specific cancellation, result values or event-loop control are useful.
Why JavaScript May Sleep Longer Than Requested
A timer delay is a lower bound, not an appointment. sleep(100) requests that the continuation become eligible after 100 milliseconds; it does not guarantee that the continuation runs at exactly that point.
Several kinds of delay can come after the requested wait:
- Synchronous JavaScript may still occupy the thread.
- Other queued work may run before the timer callback.
- After five nested browser timers, the timer algorithm applies a minimum delay of 4 milliseconds.
- Browsers may throttle timers in inactive tabs.
- The browser or operating system may be busy.
A zero-delay timer follows the same rule. It queues work for later rather than inserting it into the current call stack.
Busy-waiting does the opposite of the sleep helper and prevents useful progress:
function blockingWait(milliseconds) {
const end = Date.now() + milliseconds;
while (Date.now() < end) {
// the JavaScript thread stays occupied
}
}
This loop does not yield to timers, input handlers or Promise continuations. If code waits for a timer while holding the thread in a loop, the timer callback cannot run until the loop releases it.
That is the central event-loop rule: a timer can make a callback eligible, but it cannot make a busy thread execute that callback.
When Sleep Is the Wrong Tool
Sleep is suitable for an intentional delay, including a visible pause or retry backoff. Other timing problems name a different event that the program actually cares about.
| Goal | Use | Why |
|---|---|---|
| Delay one async continuation | Promise-based sleep | The code intentionally waits for a minimum duration |
| Add retry backoff | Cancellable sleep between attempts | The delay is part of the retry policy and can stop with the operation |
| Give an operation a deadline | AbortSignal.timeout(...) | It creates a cancellation signal when time runs out; it is not a sleep function |
| Update browser animation | requestAnimationFrame | The browser invokes the callback before a repaint |
| Wait for state to change | An event, or condition-aware polling | The condition matters more than an arbitrary elapsed duration |
| Synchronize supported worker code | Atomics.wait | It blocks on shared memory and is not available on the browser main thread |
A deadline must be connected to the operation being stopped. Racing an operation against sleep with Promise.race settles the race, but it does not cancel the losing operation.
Animation has its own clock. requestAnimationFrame asks the browser to call code before the next repaint and is generally paused in background tabs or hidden iframes. JavaScript animations shows the frame loop, and The Browser Platform places that loop beside the rest of the browser APIs it depends on.
State changes deserve state-aware code. Prefer an event when the producer can announce the change. If polling is necessary, check the condition between cancellable delays and stop when its signal aborts.
Atomics.wait is different again. It is a blocking synchronization primitive for shared-memory typed arrays, and a browser main thread cannot use it. It belongs in supported worker synchronization code, not in a general-purpose JavaScript sleep helper.