DEV Community

Kalyan P C
Kalyan P C

Posted on

Basic Asynchronous JavaScript

In standard JavaScript, code runs line-by-line, waiting for each task to finish before moving to the next. Asynchronous JavaScript breaks this "blocking" behavior, allowing your application to handle time-consuming tasks—like fetching data or loading images—without freezing the user interface.

1. JavaScript is single-threaded

There is only one main thread that executes JavaScript code.
That means only one piece of code can run at a time.

console.log("A");
console.log("B");
console.log("C");
// → A → B → C  (always in this exact order)
Enter fullscreen mode Exit fullscreen mode

2. Most interesting things are asynchronous

Things that take time usually don't block the main thread:

  • setTimeout, setInterval
  • DOM events (click, input, scroll…)
  • Network requests (fetch, XMLHttpRequest)
  • Promise resolution
  • File reading/writing (Node.js)
  • Database queries (Node.js)

These operations are handed off to the browser (Web APIs) or to libuv (Node.js), and JavaScript continues running immediately.

3. Classic example – setTimeout(…, 0)

Even with delay = 0, the callback does not run immediately.

console.log("1 - sync");

setTimeout(() => {
  console.log("3 - timeout callback");
}, 0);

console.log("2 - sync");

// Output:
// 1 - sync
// 2 - sync
// 3 - timeout callback
Enter fullscreen mode Exit fullscreen mode

Why?
setTimeout is a macrotask.
Even with 0 ms delay, it goes to the task queue and only runs after the current call stack is empty.

4. Promises run faster than setTimeout (microtasks vs macrotasks)

console.log("1");

setTimeout(() => console.log("5 - macrotask"), 0);

Promise.resolve()
  .then(() => console.log("3 - microtask"))
  .then(() => console.log("4 - microtask"));

console.log("2");

// Output:
// 1
// 2
// 3 - microtask
// 4 - microtask
// 5 - macrotask
Enter fullscreen mode Exit fullscreen mode

Example to understand the execution flow

async function asyncTask() {
  console.log('Async Start'); // 2. Synchronous
  await Promise.resolve();    // Execution pauses here, rest moves to Microtask Queue
  console.log('Async End');   // 4. Microtask
}

console.log('Start');         // 1. Synchronous

setTimeout(() => {
  console.log('Timeout 1');   // 6. Macrotask

  setTimeout(() => {
    console.log('Timeout 2'); // 10. Macrotask (queued after T1 & T3 runs)
  }, 0);

  Promise.resolve().then(() => {
    console.log('Promise 1'); // 7. Microtask (runs before next Macrotask)
  });
}, 0);

asyncTask();

Promise.resolve().then(() => {
  console.log('Promise 2');   // 5. Microtask
});

setTimeout(() => {
  console.log('Timeout 3');   // 8. Macrotask

  setTimeout(() => {
    console.log('Timeout 4'); // 11. Macrotask (queued after T1 & T3 & T2 runs)
  }, 0);

  Promise.resolve().then(() => {
    console.log('Promise 2'); // 9. Microtask (runs before next Macrotask)
  });
}, 0);


console.log('End');           // 3. Synchronous
Enter fullscreen mode Exit fullscreen mode

Code Playground:

Task Priority Table

Type Queue Priority When it runs Examples
Synchronous code Call stack Highest Immediately console.log, variable assignments
Microtasks Microtask queue Very high After current task, before next macrotask Promise.then, queueMicrotask, MutationObserver
Macrotasks Macrotask / Task queue Normal One at a time after microtasks are empty setTimeout, setInterval, DOM events, I/O

Key Takeaway:

  • Synchronous code runs first
  • Then all microtasks are drained (Promise callbacks, queueMicrotask, MutationObserver)
  • Then one macrotask is executed (setTimeout, setInterval, events, I/O callbacks…)
  • Then again all microtasks, then next macrotask, and so on…

The Event Loop will always exhaust the Microtask Queue before moving on to the next item in the Macrotask Queue. This is why a promise resolution will almost always execute before a setTimeout(..., 0).

Thanks for reading. Happy coding!!!

Top comments (1)

Collapse
 
a-k-0047 profile image
ak0047

Really clear and beginner friendly article — thank you!