When learning JavaScript, one of the most confusing — yet essential — concepts is understanding how synchronous and asynchronous code works.
In this blog post, I’ll explain the differences clearly, with code snippets, and simple explanations that anyone can follow.
Code Example: Sync vs Async in Action
console.log("Hi!");
setTimeout(() => {
console.log("I am inside setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("I am inside Promise");
});
console.log("Bye!");
You might guess the output of the above code would be:
Hi!
I am inside setTimeout
I am inside Promise
Bye!
But the actual output is:
Hi!
Bye!
I am inside Promise
I am inside setTimeout
🧠 JavaScript is Single-Threaded
JavaScript runs on a single thread, meaning only one task executes at a time using a structure called the Call Stack. However, JavaScript handles asynchronous operations (like timers or API calls) through:
-Web APIs (provided by the browser)
-Callback Queue (aka Macrotask Queue)
-Microtask Queue (Promises and MutationObservers)
–The Event Loop
Synchronous Code Explained
Synchronous code executes line by line, directly on the call stack:
console.log("Hi!");
console.log("Bye!");
These statements go to the call stack immediately and execute in order:
Hi!
Bye!
Asynchronous Code: setTimeout()
and Promise
Let’s break it down:
setTimeout
setTimeout(() => {
console.log("I am inside setTimeout");
}, 0);
-setTimeout() is called and enters the call stack.
-It’s passed to the browser’s Web API, which starts a timer.
-After the timer expires (even with 0ms), it moves to the callback (macrotask) queue.
-The event loop checks if the call stack is empty and then moves it to the call stack for execution.
Promise
Promise.resolve().then(() => {
console.log("I am inside Promise");
});
-The Promise.resolve().then(…) enters the call stack.
-Then it is added to the microtask queue by JavaScript engine
-After all synchronous code runs, microtasks are given priority and executed before any macrotasks.
So the Promise message appears before setTimeout even though both were set with no delay.
How the Event Loop Works (Simplified)
The Event Loop constantly checks:
- Is the call stack empty?
- If yes, are there any microtasks in the queue?
- If yes, move them one by one to the call stack and run them.
- After microtasks are done, check the macrotask queue.
- Move them one by one to the call stack and run them.
Note – It’s not the event loop that places Promises in the microtask queue — the JavaScript engine does it when a Promise resolves.
Final Execution Order Breakdown
console.log("Hi!")
→ Synchronous → call stackconsole.log("Bye!")
→ Synchronous → call stackPromise.then()
→ Microtask queue → runs after syncsetTimeout()
→ Macrotask queue → runs after microtasks
So the final output is:
Hi!
Bye!
I am inside Promise
I am inside setTimeout
Summary Table for Reference
Code Type | Goes to Call Stack? | Offloaded To | Queue Type | Runs When? |
console.log() | ✅ Yes | ❌ None | None | Immediately (during sync execution) |
setTimeout() | ✅ Yes | ✅ Web API | Macrotask Queue | After all microtasks complete |
Promise.then() | ✅ Yes | ✅ JS Engine (async job) | Microtask Queue | Right after synchronous code ends |
Conclusion
JavaScript is single-threaded but handles complex asynchronous operations using Web APIs, task queues, and the event loop. Understanding this flow helps you debug timing issues and build better code.
Key Takeaways:
- Synchronous code runs first.
- Promises (microtasks) run before setTimeout (macrotasks).
setTimeout(..., 0)
still doesn’t run immediately!