JavaScript
async/await
setTimeout
programming
asynchronous

async/await function does not wait for setTimeout to finish

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

await only works with Promises, but setTimeout does not return a Promise β€” it returns a numeric timer ID. Writing await setTimeout(fn, 1000) does not pause execution for 1 second. The await resolves immediately with the timer ID, and the callback runs later. The fix is to wrap setTimeout in a Promise so that await can properly wait for the delay to complete.

The Problem

javascript
1async function doWork() {
2  console.log("Start");
3  await setTimeout(() => {
4    console.log("Timeout done");
5  }, 2000);
6  console.log("End");
7}
8
9doWork();
10// Output:
11// Start
12// End          ← runs immediately, does not wait
13// Timeout done ← runs 2 seconds later

await expects a Promise (a thenable). setTimeout returns a number (the timer ID). When you await a non-Promise value, JavaScript wraps it in Promise.resolve(timerId), which resolves instantly.

The Fix: Wrap setTimeout in a Promise

javascript
1function delay(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5async function doWork() {
6  console.log("Start");
7  await delay(2000);
8  console.log("End"); // runs after 2 seconds
9}
10
11doWork();
12// Output:
13// Start
14// (2 second pause)
15// End

The delay function creates a Promise that resolves when setTimeout fires. Now await has a real Promise to wait on.

Using Node.js Built-In timers/promises

Node.js 16+ provides a Promise-based setTimeout out of the box:

javascript
1import { setTimeout } from 'timers/promises';
2
3async function doWork() {
4  console.log("Start");
5  await setTimeout(2000); // Note: delay is the first argument, no callback
6  console.log("End");
7}
8
9doWork();
10// Start
11// (2 second pause)
12// End

This is the cleanest approach in Node.js β€” no need to write your own wrapper.

Running Code After the Delay

If you need to run logic after the delay:

javascript
1function delay(ms) {
2  return new Promise(resolve => setTimeout(resolve, ms));
3}
4
5async function fetchWithRetry(url, retries = 3) {
6  for (let i = 0; i < retries; i++) {
7    try {
8      const response = await fetch(url);
9      if (response.ok) return await response.json();
10    } catch (err) {
11      console.log(`Attempt ${i + 1} failed, retrying...`);
12    }
13    await delay(1000 * (i + 1)); // Wait 1s, 2s, 3s between retries
14  }
15  throw new Error(`Failed after ${retries} retries`);
16}

Cancellable Delay

Sometimes you need to cancel a pending delay:

javascript
1function cancellableDelay(ms) {
2  let timerId;
3  const promise = new Promise((resolve, reject) => {
4    timerId = setTimeout(resolve, ms);
5  });
6  return {
7    promise,
8    cancel: () => clearTimeout(timerId)
9  };
10}
11
12async function example() {
13  const { promise, cancel } = cancellableDelay(5000);
14
15  // Cancel after 1 second
16  setTimeout(() => {
17    cancel();
18    console.log("Delay cancelled");
19  }, 1000);
20
21  await promise; // This will hang since resolve is never called after cancel
22}

With AbortController (Node.js timers/promises):

javascript
1import { setTimeout } from 'timers/promises';
2
3const controller = new AbortController();
4
5// Cancel after 1 second
6setTimeout(1000).then(() => controller.abort());
7
8try {
9  await setTimeout(5000, null, { signal: controller.signal });
10} catch (err) {
11  if (err.code === 'ABORT_ERR') {
12    console.log("Timer was cancelled");
13  }
14}

Why setInterval Has the Same Issue

javascript
1// This does NOT work β€” same reason as setTimeout
2async function poll() {
3  await setInterval(() => {
4    console.log("tick");
5  }, 1000);
6  console.log("done"); // runs immediately
7}
8
9// Use a loop with delay instead
10async function poll() {
11  for (let i = 0; i < 5; i++) {
12    console.log("tick");
13    await new Promise(resolve => setTimeout(resolve, 1000));
14  }
15  console.log("done");
16}

Common Pitfalls

  • Assuming await pauses any asynchronous function: await only pauses on Promises. Callback-based APIs like setTimeout, setInterval, fs.readFile (callback version), and event listeners do not return Promises, so await has no effect on them.
  • Wrapping the callback, not the delay: Writing await new Promise(resolve => setTimeout(() => { doStuff(); resolve(); }, 1000)) works but is verbose. Separate the delay from the logic β€” await delay(1000); doStuff(); is cleaner.
  • Forgetting that setTimeout(fn, 0) is not instant: setTimeout(fn, 0) still defers fn to the next iteration of the event loop. Wrapping it in a Promise and awaiting it introduces a microtask + macrotask boundary, which can affect ordering with other Promises.
  • Using await in a forEach callback: Array.forEach ignores the return value (and thus the Promise) from async callbacks. Use for...of or Promise.all(arr.map(...)) instead for sequential or parallel async operations.
  • Not handling errors in delayed operations: If the code after await delay(ms) throws, the error propagates as an unhandled Promise rejection unless the caller uses try/catch or .catch().

Summary

  • setTimeout returns a timer ID, not a Promise β€” await does not wait for it
  • Wrap setTimeout in a Promise: new Promise(resolve => setTimeout(resolve, ms))
  • In Node.js 16+, use import { setTimeout } from 'timers/promises' for a built-in Promise-based delay
  • Use AbortController with timers/promises for cancellable delays
  • Replace setInterval with for loops and await delay() for async-friendly polling
  • Only await values that are Promises β€” callback APIs require wrapping first

Course illustration
Course illustration

All Rights Reserved.