JavaScript
async functions
synchronization
sleep function
coding techniques

JavaScript - sync wait for async operation sleep

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

JavaScript runs on an event loop, so developers often ask how to “wait” synchronously for async work. The short answer is that true blocking waits are almost always the wrong tool in application code. A better approach is structured async flow using await, explicit timeouts, and predictable retry logic.

Why Blocking Sleep Is a Problem in JavaScript

In browser and Node.js runtimes, one thread handles most user-land JavaScript execution. If you block that thread with a busy loop, timers, network callbacks, rendering, and user events stop progressing. The app looks frozen even though your code is still running.

A bad pattern looks like this:

javascript
1function block(ms) {
2  const end = Date.now() + ms;
3  while (Date.now() < end) {
4    // busy wait
5  }
6}
7
8console.log('start');
9block(2000);
10console.log('end');

This appears to “sleep,” but it blocks everything else. In UI code, this can cause dropped frames and unresponsive controls. In server code, it can block concurrent request handling on that event loop thread.

Use Promise-Based Sleep with await

The standard non-blocking sleep pattern wraps setTimeout in a Promise.

javascript
1function sleep(ms) {
2  return new Promise((resolve) => setTimeout(resolve, ms));
3}
4
5async function run() {
6  console.log('start');
7  await sleep(1000);
8  console.log('after 1 second');
9}
10
11run();

This pauses only the current async function while allowing the runtime to process other work. That is usually what people actually want when they say “sync wait for async.”

Coordinating Async Steps Reliably

Real workflows often need a sequence of async actions with controlled delays, for example polling a job status endpoint. Keep this logic explicit and linear.

javascript
1async function pollStatus(fetchStatus, maxAttempts = 5) {
2  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
3    const status = await fetchStatus();
4    console.log(`attempt ${attempt}: ${status}`);
5
6    if (status === 'done') {
7      return { ok: true, attempts: attempt };
8    }
9
10    await sleep(1000);
11  }
12
13  return { ok: false, attempts: maxAttempts };
14}
15
16// Demo with a fake API
17let count = 0;
18async function fakeStatus() {
19  count += 1;
20  return count >= 3 ? 'done' : 'pending';
21}
22
23pollStatus(fakeStatus).then(console.log);

This pattern is easy to test and reason about because control flow is straightforward.

Add Timeout Control with Promise.race

Sometimes async operations can hang longer than acceptable. Wrap operations in a timeout so failures are explicit.

javascript
1function withTimeout(promise, ms) {
2  const timeout = new Promise((_, reject) => {
3    setTimeout(() => reject(new Error('operation timed out')), ms);
4  });
5
6  return Promise.race([promise, timeout]);
7}
8
9async function demoTimeout() {
10  const slowTask = sleep(3000).then(() => 'finished');
11  try {
12    const result = await withTimeout(slowTask, 1000);
13    console.log(result);
14  } catch (err) {
15    console.error(err.message);
16  }
17}
18
19demoTimeout();

Timeouts prevent hidden stalls from propagating through the system and simplify observability.

Practical Patterns for Production Code

In production systems, delay logic should be reusable and cancellable. Wrapping sleep in a helper that supports cancellation prevents background work from continuing after a user navigates away or a request is aborted.

javascript
1function sleepWithSignal(ms, signal) {
2  return new Promise((resolve, reject) => {
3    const id = setTimeout(resolve, ms);
4    if (signal) {
5      signal.addEventListener('abort', () => {
6        clearTimeout(id);
7        reject(new Error('sleep cancelled'));
8      }, { once: true });
9    }
10  });
11}
12
13const controller = new AbortController();
14setTimeout(() => controller.abort(), 200);
15
16sleepWithSignal(1000, controller.signal)
17  .then(() => console.log('completed'))
18  .catch((err) => console.log(err.message));

This gives you clean cancellation semantics and makes long-lived async loops safer. It also reduces hidden resource usage because timers are cleaned up when aborted.

Common Pitfalls

Busy-wait loops are the biggest mistake. They consume CPU and block the event loop, which defeats JavaScript’s async model.

Another common issue is forgetting await in front of sleep calls. If you call sleep(1000) without awaiting or returning the Promise, execution continues immediately.

Error handling is also often overlooked. Delays and retries should include a maximum attempt count and a clear failure path.

Finally, avoid using artificial sleeps as a substitute for proper readiness signals. Prefer awaiting explicit events, status checks, or completion callbacks where possible.

Summary

  • JavaScript should avoid blocking sleeps in normal application code.
  • Use Promise-based sleep and await for non-blocking delays.
  • Structure async workflows as explicit sequences with retry limits.
  • Add timeouts so long-running tasks fail predictably.
  • Prefer real completion signals over arbitrary delay values.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

All Rights Reserved.