JavaScript Sleep: How to Pause Without Blocking

Aug 14, 2026·18 min read

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:

Where await may appearClassic scriptasyncfunctionawaitneeds a containerModuleawaittop level is valid
Where 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:

  1. loadPreview() runs until it reaches await sleep(20).
  2. The sleep helper creates a timer and returns a pending Promise.
  3. The call stack becomes available, so the main code and unrelated timer can run.
  4. When the sleep timer runs, it resolves the Promise. The suspended part of loadPreview becomes a Promise continuation, scheduled as a microtask.

That continuation prints preview continued. Nothing held the thread for 20 milliseconds. The function stepped aside.

One function steps asideloadPreviewstartssleep: continuation suspendedcontinuessharedthreadmain codeother timerthe thread keeps serving other work
An awaited sleep pauses one continuation while other work uses the thread.

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.

Same delays, different shapefor…ofin orderdraftreviewpublishabout 60 ms totalPromise.alltogetherdraftreviewpublishabout 20 msfor the group
Sequential waits add their durations; concurrent waits share the same time span.

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.aborted handles cancellation that happened before sleep was called.
  • clearTimeout(timerId) prevents the timer callback from running after cancellation wins.
  • reject(signal.reason) preserves the caller’s reason for stopping.
  • removeEventListener releases 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.

Two ways to settle sleeppending Promisetimer winsabort winsremove abortlistenerclear pendingtimerresolvereject reasonone winner, one cleanup path
An abortable sleep must clean up whichever path loses.

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.

Does the timer hold Node open?ref: trueNode processtimertimer is a tetherref: falseNodeprocesstimerprocess may exit
A referenced timer holds the process open; an unreferenced timer does not.

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.

Eligible is not the same as running0 ms100 mslaterminimum waitwaiting in queueJavaScript thread is busycallbackrunstimer becomes eligible here
The requested delay controls eligibility, not the exact execution time.

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.

Choose by what ends the waitWhat musthappen beforecode continues?Elapsed timesleepNext repaintanimation frameState changedeventDeadline hitabort signalShared memoryAtomics.waitThe ending conditionselects the tool.
Choose the tool by the event that should end the wait.
GoalUseWhy
Delay one async continuationPromise-based sleepThe code intentionally waits for a minimum duration
Add retry backoffCancellable sleep between attemptsThe delay is part of the retry policy and can stop with the operation
Give an operation a deadlineAbortSignal.timeout(...)It creates a cancellation signal when time runs out; it is not a sleep function
Update browser animationrequestAnimationFrameThe browser invokes the callback before a repaint
Wait for state to changeAn event, or condition-aware pollingThe condition matters more than an arbitrary elapsed duration
Synchronize supported worker codeAtomics.waitIt 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.

Frequently asked questions

How do you sleep in JavaScript?
Create a Promise that resolves through setTimeout, then await that Promise. The delay uses milliseconds and pauses only the surrounding async function, so other JavaScript can continue running.
Does JavaScript sleep block the browser?
An awaited Promise-based sleep does not block the browser's main thread. It suspends one async continuation while timers, events and other code continue to run.
Can a JavaScript sleep be cancelled?
A sleep function can accept an AbortSignal, clear its timer when the signal aborts and reject with signal.reason. It must also handle a signal that was already aborted and remove its abort listener after a successful delay.
Why does setTimeout run later than requested?
The delay passed to setTimeout is a minimum wait, not an exact execution time. Busy code, queued tasks, nested timer limits and inactive-tab throttling can all make the callback run later.
What is the Node.js equivalent of sleep?
Node.js provides a Promise-returning setTimeout through node:timers/promises. It accepts a result value, an AbortSignal and a ref option that controls whether the timer keeps the event loop active.