JavaScript is often described using words like "single-threaded," "asynchronous," and "non-blocking" — but what do these really mean? In this article, we’ll break down how JavaScript executes functions and manages concurrency using the call stack, event loop, microtasks, macrotasks, and more. Whether you're a beginner or aiming to be a senior developer, understanding these mechanics is essential for writing efficient and predictable JavaScript code.
1. The Call Stack: Where Execution Happens
The call stack is a data structure that keeps track of function calls. When a function is called, it's added to the stack (pushed). When it finishes, it's removed (popped). JavaScript runs code top to bottom and uses the stack to manage function execution order.
function greet(name) {
console.log("Hello, " + name);
}
greet("Alice");
In this example, greet("Alice")
is pushed to the stack. Once console.log
runs, it is popped off. The stack ensures that only one function executes at a time.
2. Memory Heap
This is where JavaScript stores memory for variables, objects, and functions. It's an unstructured region and complements the call stack by holding references used during execution.
3. Web APIs and Asynchronous Behavior
JavaScript offloads certain operations to the browser or runtime environment (like timers, DOM events, and HTTP requests). These are handled by Web APIs, and once completed, they queue a callback to be executed later.
console.log("Start");
setTimeout(() => {
console.log("Timeout finished");
}, 1000);
console.log("End");
Output:
Start
End
Timeout finished
4. The Event Loop
The event loop constantly checks if the call stack is empty. If it is, it takes the next message from the task queue and pushes it onto the stack.
5. Tasks vs Microtasks
Macrotasks (Tasks):
Include things like:
setTimeout
setInterval
- I/O operations
- UI rendering
Microtasks:
Include:
- Promises
queueMicrotask
MutationObserver
Microtasks are executed after the current function but before the next macrotask.
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
Promise.resolve().then(() => {
console.log("promise1");
}).then(() => {
console.log("promise2");
});
console.log("script end");
Output:
script start
script end
promise1
promise2
setTimeout
6. Promises and Async/Await
Promises represent a value that may be available now, or in the future. They're always asynchronous.
async function fetchData() {
const res = await fetch("https://api.example.com/data");
const data = await res.json();
console.log(data);
}
fetchData();
await
pauses execution in that function until the Promise resolves, but it doesn't block the entire thread.
7. Node.js Event Loop Phases
Node.js has a slightly more complex event loop with phases:
- Timers
- Pending callbacks
- Idle, prepare
- Poll
- Check (
setImmediate
) - Close callbacks
Additionally, Node.js has process.nextTick
, which runs before any microtasks.
process.nextTick(() => console.log("nextTick"));
Promise.resolve().then(() => console.log("promise"));
setTimeout(() => console.log("timeout"), 0);
Output:
nextTick
promise
timeout
8. Common Mistakes and Best Practices
- Avoid long-running synchronous code
- Prefer
async/await
over deeply nested callbacks - Know when to use
setTimeout
vsPromise
- Don’t abuse
process.nextTick
9. Comparison with Other Languages
- Java/Threads: Multi-threaded, needs synchronization
- Python (asyncio): Similar event-loop concept
- Go (goroutines): True concurrency
JavaScript avoids race conditions by running all code in a single thread and using event queues to manage async behavior.
10. Conclusion
JavaScript’s event-driven, single-threaded execution model is both powerful and efficient — once you understand it. Mastering the call stack, event loop, and task queues will improve your ability to write responsive and scalable JavaScript applications.
Happy coding!
Download Book
Top comments (0)