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)
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
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
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
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)
Really clear and beginner friendly article — thank you!