JavaScript doesn’t have a blocking sleep() function like some other languages. Trying to fake one with a long while loop may delay the next line, but it also freezes buttons, animations and the rest of the page.
The better approach is to create a Promise-based delay and use it with async/await. Only the current async task pauses while the browser stays responsive.
This example includes a cancelable delay, progress display and event log. Watch the live clock while the sequence is running — it keeps moving because the page isn’t blocked.
No library needed.
This is part of Copy, Run & Customize, a series of small projects you can open, change and use in your own work.
Copy the three blocks below, then open the TeknoFenomen Code Playground to run and edit it.
HTML
<main class="delay-page">
<section class="delay-card">
<header class="tool-header">
<span class="eyebrow">ASYNC JAVASCRIPT</span>
<h1>Sleep without freezing the page</h1>
<p>
Run a sequence of delayed tasks, watch the event loop
stay responsive and cancel the operation at any time.
</p>
</header>
<div class="live-clock-card">
<div>
<span>LIVE BROWSER CLOCK</span>
<strong id="liveClock">00:00:00.0</strong>
</div>
<p>
If this clock keeps moving, the JavaScript thread
isn’t blocked.
</p>
</div>
<div class="controls-grid">
<label class="control">
<span>Delay between steps</span>
<div class="number-field">
<input
id="delayInput"
type="number"
min="100"
max="10000"
step="100"
value="1000"
>
<span>ms</span>
</div>
</label>
<label class="control">
<span>Number of steps</span>
<select id="stepSelect">
<option value="3">3 steps</option>
<option value="5" selected>5 steps</option>
<option value="8">8 steps</option>
<option value="10">10 steps</option>
</select>
</label>
</div>
<div class="main-actions">
<button id="startButton" class="primary-button" type="button">
Start sequence
</button>
<button id="cancelButton" type="button" disabled>
Cancel
</button>
<button id="clearButton" type="button">
Clear log
</button>
</div>
<div
id="statusBox"
class="status-box neutral"
role="status"
aria-live="polite"
>
<span class="status-dot"></span>
<div>
<strong id="statusTitle">Ready</strong>
<p id="statusMessage">
Choose a delay and start the sequence.
</p>
</div>
</div>
<section class="progress-section">
<div class="progress-heading">
<span>Progress</span>
<strong id="progressText">0 / 5</strong>
</div>
<div
class="progress-track"
role="progressbar"
aria-label="Sequence progress"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
id="progressTrack"
>
<span id="progressBar"></span>
</div>
</section>
<section class="log-panel">
<div class="log-heading">
<div>
<strong>Event log</strong>
<span>Each entry appears after an awaited delay</span>
</div>
<span id="logCount">0 events</span>
</div>
<ol id="eventLog" class="event-log">
<li class="empty-log">
Run the sequence to see the events here.
</li>
</ol>
</section>
</section>
</main>
CSS
:root {
color-scheme: dark;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system,
BlinkMacSystemFont, "Segoe UI", sans-serif;
--background: #0c0906;
--panel: #19130f;
--panel-light: #261d16;
--input: #100c09;
--border: #463528;
--text: #fff4e9;
--muted: #b6a392;
--accent: #ff9f43;
--accent-hover: #ffb366;
--success: #52dc98;
--danger: #ff6f7d;
--running: #ffd166;
}
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top, #442612 0, transparent 42%),
var(--background);
color: var(--text);
}
button,
input,
select {
font: inherit;
}
button {
padding: 10px 15px;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--panel-light);
color: var(--text);
cursor: pointer;
transition:
border-color 0.2s,
background 0.2s,
transform 0.15s;
}
button:hover:not(:disabled) {
border-color: #76563d;
background: #34271e;
}
button:active:not(:disabled) {
transform: translateY(1px);
}
button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
button:focus-visible,
input:focus-visible,
select:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.delay-page {
width: min(860px, calc(100% - 30px));
margin: 0 auto;
padding: 50px 0;
}
.delay-card {
padding: 28px;
border: 1px solid var(--border);
border-radius: 20px;
background: rgba(25, 19, 15, 0.96);
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.4);
}
.tool-header {
max-width: 680px;
margin-bottom: 24px;
}
.eyebrow {
color: #ffc078;
font-size: 0.72rem;
font-weight: 800;
letter-spacing: 0.16em;
}
h1 {
margin: 8px 0 10px;
font-size: clamp(1.8rem, 6vw, 2.8rem);
line-height: 1.1;
}
.tool-header p {
margin: 0;
color: var(--muted);
line-height: 1.65;
}
.live-clock-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
padding: 18px;
border: 1px solid rgba(255, 159, 67, 0.45);
border-radius: 14px;
background:
linear-gradient(
135deg,
rgba(255, 159, 67, 0.13),
transparent 65%
),
var(--input);
}
.live-clock-card > div {
display: flex;
flex: 0 0 auto;
flex-direction: column;
gap: 5px;
}
.live-clock-card span {
color: #ffc078;
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.12em;
}
.live-clock-card strong {
font-family:
"SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: clamp(1.4rem, 5vw, 2rem);
}
.live-clock-card p {
max-width: 340px;
margin: 0;
color: var(--muted);
font-size: 0.82rem;
line-height: 1.5;
text-align: right;
}
.controls-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 13px;
margin-top: 16px;
}
.control {
display: flex;
min-width: 0;
flex-direction: column;
gap: 8px;
padding: 14px;
border: 1px solid var(--border);
border-radius: 11px;
background: var(--input);
}
.control > span {
font-size: 0.83rem;
font-weight: 700;
}
.control input,
.control select {
width: 100%;
height: 42px;
padding: 0 11px;
border: 1px solid var(--border);
border-radius: 8px;
outline: 0;
background: var(--panel-light);
color: var(--text);
}
.control input:focus,
.control select:focus {
border-color: var(--accent);
}
.number-field {
position: relative;
}
.number-field input {
padding-right: 48px;
}
.number-field span {
position: absolute;
top: 50%;
right: 12px;
color: var(--muted);
font-size: 0.77rem;
transform: translateY(-50%);
pointer-events: none;
}
.main-actions {
display: flex;
flex-wrap: wrap;
gap: 9px;
margin-top: 15px;
}
.primary-button {
flex: 1;
min-width: 160px;
border-color: var(--accent);
background: var(--accent);
color: #2a1402;
font-weight: 800;
}
.primary-button:hover:not(:disabled) {
border-color: var(--accent-hover);
background: var(--accent-hover);
}
.status-box {
display: flex;
align-items: flex-start;
gap: 12px;
margin-top: 16px;
padding: 14px 15px;
border: 1px solid var(--border);
border-radius: 12px;
background: var(--input);
}
.status-dot {
flex: 0 0 auto;
width: 10px;
height: 10px;
margin-top: 5px;
border-radius: 50%;
background: var(--muted);
}
.status-box strong {
display: block;
margin-bottom: 3px;
}
.status-box p {
margin: 0;
color: var(--muted);
font-size: 0.88rem;
line-height: 1.5;
}
.status-box.running {
border-color: rgba(255, 209, 102, 0.5);
}
.status-box.running .status-dot {
background: var(--running);
box-shadow: 0 0 12px rgba(255, 209, 102, 0.6);
animation: pulse 1s infinite;
}
.status-box.success {
border-color: rgba(82, 220, 152, 0.45);
}
.status-box.success .status-dot {
background: var(--success);
box-shadow: 0 0 12px rgba(82, 220, 152, 0.6);
}
.status-box.error {
border-color: rgba(255, 111, 125, 0.5);
}
.status-box.error .status-dot {
background: var(--danger);
box-shadow: 0 0 12px rgba(255, 111, 125, 0.55);
}
@keyframes pulse {
50% {
opacity: 0.35;
}
}
.progress-section {
margin-top: 20px;
}
.progress-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
margin-bottom: 8px;
color: var(--muted);
font-size: 0.78rem;
}
.progress-heading strong {
color: var(--text);
}
.progress-track {
height: 11px;
overflow: hidden;
border: 1px solid var(--border);
border-radius: 20px;
background: var(--input);
}
.progress-track span {
display: block;
width: 0;
height: 100%;
border-radius: inherit;
background:
linear-gradient(90deg, var(--accent), #ffd166);
transition: width 0.3s ease;
}
.log-panel {
overflow: hidden;
margin-top: 18px;
border: 1px solid var(--border);
border-radius: 13px;
background: var(--input);
}
.log-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 15px;
padding: 12px 14px;
border-bottom: 1px solid var(--border);
background: var(--panel-light);
}
.log-heading > div {
display: flex;
flex-direction: column;
gap: 3px;
}
.log-heading strong {
font-size: 0.87rem;
}
.log-heading span {
color: var(--muted);
font-size: 0.72rem;
}
.event-log {
min-height: 185px;
max-height: 350px;
overflow: auto;
margin: 0;
padding: 12px;
list-style: none;
}
.event-log li {
display: grid;
grid-template-columns: 82px 1fr;
gap: 12px;
padding: 10px;
border-bottom: 1px solid rgba(70, 53, 40, 0.7);
font-family:
"SFMono-Regular", Consolas, "Liberation Mono", monospace;
font-size: 0.78rem;
line-height: 1.45;
}
.event-log li:last-child {
border-bottom: 0;
}
.event-log time {
color: var(--muted);
}
.event-log .waiting {
color: #ffd166;
}
.event-log .complete {
color: var(--success);
}
.event-log .cancelled {
color: var(--danger);
}
.event-log .empty-log {
display: block;
padding: 55px 15px;
border: 0;
color: var(--muted);
font-family: inherit;
text-align: center;
}
@media (max-width: 610px) {
.delay-page {
width: min(100% - 20px, 860px);
padding: 20px 0;
}
.delay-card {
padding: 18px;
}
.live-clock-card {
align-items: flex-start;
flex-direction: column;
}
.live-clock-card p {
text-align: left;
}
.controls-grid {
grid-template-columns: 1fr;
}
}
@media (max-width: 430px) {
.main-actions {
display: grid;
grid-template-columns: 1fr 1fr;
}
.primary-button {
grid-column: 1 / -1;
}
.event-log li {
grid-template-columns: 1fr;
gap: 4px;
}
}
JavaScript
const delayInput = document.querySelector("#delayInput");
const stepSelect = document.querySelector("#stepSelect");
const startButton = document.querySelector("#startButton");
const cancelButton = document.querySelector("#cancelButton");
const clearButton = document.querySelector("#clearButton");
const liveClock = document.querySelector("#liveClock");
const statusBox = document.querySelector("#statusBox");
const statusTitle = document.querySelector("#statusTitle");
const statusMessage = document.querySelector("#statusMessage");
const progressTrack = document.querySelector("#progressTrack");
const progressBar = document.querySelector("#progressBar");
const progressText = document.querySelector("#progressText");
const eventLog = document.querySelector("#eventLog");
const logCount = document.querySelector("#logCount");
let activeController = null;
let eventCount = 0;
function setStatus(type, title, message) {
statusBox.className = `status-box ${type}`;
statusTitle.textContent = title;
statusMessage.textContent = message;
}
function updateClock() {
const now = new Date();
const time = now.toLocaleTimeString();
const tenth = Math.floor(now.getMilliseconds() / 100);
liveClock.textContent = `${time}.${tenth}`;
}
function updateProgress(completed, total) {
const percentage =
total === 0 ? 0 : Math.round((completed / total) * 100);
progressBar.style.width = `${percentage}%`;
progressText.textContent = `${completed} / ${total}`;
progressTrack.setAttribute(
"aria-valuenow",
String(percentage)
);
}
function getLogTime() {
return new Date().toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
});
}
function addLog(message, type = "") {
const emptyLog = eventLog.querySelector(".empty-log");
if (emptyLog) {
emptyLog.remove();
}
const item = document.createElement("li");
const time = document.createElement("time");
const text = document.createElement("span");
time.textContent = getLogTime();
text.textContent = message;
if (type) {
text.className = type;
}
item.append(time, text);
eventLog.append(item);
eventCount += 1;
logCount.textContent =
`${eventCount} ${eventCount === 1 ? "event" : "events"}`;
eventLog.scrollTop = eventLog.scrollHeight;
}
function clearLog() {
eventLog.innerHTML = "";
const emptyMessage = document.createElement("li");
emptyMessage.className = "empty-log";
emptyMessage.textContent =
"Run the sequence to see the events here.";
eventLog.append(emptyMessage);
eventCount = 0;
logCount.textContent = "0 events";
}
function createAbortError() {
return new DOMException(
"The delay was cancelled.",
"AbortError"
);
}
function sleep(milliseconds, options = {}) {
const { signal } = options;
return new Promise((resolve, reject) => {
if (signal?.aborted) {
reject(createAbortError());
return;
}
const handleAbort = () => {
clearTimeout(timer);
reject(createAbortError());
};
const timer = setTimeout(() => {
signal?.removeEventListener(
"abort",
handleAbort
);
resolve();
}, milliseconds);
signal?.addEventListener(
"abort",
handleAbort,
{ once: true }
);
});
}
function setRunningState(running) {
startButton.disabled = running;
cancelButton.disabled = !running;
delayInput.disabled = running;
stepSelect.disabled = running;
}
async function runSequence() {
const delay = Number(delayInput.value);
const totalSteps = Number(stepSelect.value);
if (
!Number.isFinite(delay) ||
delay < 100 ||
delay > 10000
) {
setStatus(
"error",
"Invalid delay",
"Enter a delay between 100 and 10,000 milliseconds."
);
return;
}
activeController = new AbortController();
const { signal } = activeController;
clearLog();
updateProgress(0, totalSteps);
setRunningState(true);
const startedAt = performance.now();
setStatus(
"running",
"Sequence running",
`Waiting ${delay} milliseconds between each step.`
);
addLog(
`Sequence started with ${totalSteps} steps.`
);
try {
for (
let currentStep = 1;
currentStep <= totalSteps;
currentStep += 1
) {
addLog(
`Step ${currentStep}: waiting ${delay} ms...`,
"waiting"
);
await sleep(delay, { signal });
updateProgress(currentStep, totalSteps);
addLog(
`Step ${currentStep}: completed.`,
"complete"
);
}
const elapsedSeconds =
(performance.now() - startedAt) / 1000;
addLog(
`Sequence finished in ${elapsedSeconds.toFixed(2)} seconds.`,
"complete"
);
setStatus(
"success",
"Sequence complete",
`All ${totalSteps} delayed steps finished successfully.`
);
} catch (error) {
if (error.name === "AbortError") {
addLog(
"Sequence cancelled by the user.",
"cancelled"
);
setStatus(
"error",
"Sequence cancelled",
"The active delay was stopped without freezing the page."
);
} else {
addLog(
`Unexpected error: ${error.message}`,
"cancelled"
);
setStatus(
"error",
"Sequence failed",
error.message
);
}
} finally {
activeController = null;
setRunningState(false);
}
}
startButton.addEventListener("click", runSequence);
cancelButton.addEventListener("click", () => {
if (!activeController) {
return;
}
setStatus(
"running",
"Cancelling",
"Stopping the active delay..."
);
activeController.abort();
});
clearButton.addEventListener("click", () => {
if (activeController) {
return;
}
clearLog();
updateProgress(0, Number(stepSelect.value));
setStatus(
"neutral",
"Ready",
"Choose a delay and start the sequence."
);
});
stepSelect.addEventListener("change", () => {
updateProgress(0, Number(stepSelect.value));
});
setInterval(updateClock, 100);
updateClock();
updateProgress(0, Number(stepSelect.value));
The basic sleep function
For most cases, this small Promise-based function is enough:
function sleep(milliseconds) {
return new Promise((resolve) => {
setTimeout(resolve, milliseconds);
});
}
Use it inside an async function:
async function runTask() {
console.log("Starting");
await sleep(2000);
console.log("Two seconds later");
}
runTask();
await pauses only runTask(). It does not block the browser’s main thread, so buttons, scrolling and animations can continue working.
Why not use a while loop?
This is sometimes suggested as a fake sleep:
function badSleep(milliseconds) {
const endTime = Date.now() + milliseconds;
while (Date.now() < endTime) {
// Keep the thread busy
}
}
Don’t use this in browser code. The loop occupies the main thread until it finishes. During that time the page can’t repaint, react to clicks or run other JavaScript.
It also wastes CPU while doing nothing useful.
Making the delay cancelable
A normal Promise can’t be cancelled by itself. AbortController gives the delay a cancellation signal:
const controller = new AbortController();
await sleep(3000, {
signal: controller.signal
});
// Cancel from somewhere else:
controller.abort();
The complete example clears the pending timer and rejects the Promise with an AbortError when cancellation is requested.
Is the delay exact?
Not always. A timeout of 1000 milliseconds means the callback should run after at least that amount of time.
It may run later if:
- The main thread is busy
- The browser tab is in the background
- The device is under heavy load
- The browser is limiting background timers
Use timestamps such as Date.now() or performance.now() when elapsed time needs to be measured accurately.
