How JavaScript Works Behind the Scenes: Runtime, Event Loop, and Execution
JavaScript looks simple on the surface: write a function, call it, and get a result. Behind that simple syntax, however, the JavaScript engine performs several steps involving execution contexts, the call stack, memory m
JavaScript looks simple on the surface: write a function, call it, and get a result. Behind that simple syntax, however, the JavaScript engine performs several steps involving execution contexts, the call stack, memory management, queues, and the event loop.
Understanding these internal mechanics makes asynchronous JavaScript much easier to reason about. Instead of memorizing why a callback runs before or after another operation, you can understand what the runtime is actually doing and predict the output yourself.
In this article, we will walk through the JavaScript execution model using a practical example. We will explore synchronous execution, promises, timers, microtasks, macrotasks, closures, and how the event loop coordinates everything.
How JavaScript Executes Code Behind the Scenes
When JavaScript starts executing a script, the engine creates an execution context and begins evaluating the code. Function calls are tracked by the call stack, while variables and objects are managed through the engine's memory system. Synchronous operations continue on the stack until they finish, which is why a long-running synchronous function can block other JavaScript work.
Asynchronous behavior works differently. Browser and Node.js environments provide runtime APIs for timers, networking, file operations, and other tasks, while the JavaScript engine itself remains focused on executing JavaScript. When an asynchronous operation completes, its callback is placed into an appropriate queue instead of immediately interrupting the current stack.
Promises are especially important because their callbacks are scheduled as microtasks. Timer callbacks such as those created with setTimeout are generally scheduled as tasks, often called macrotasks. After the current synchronous stack becomes empty, the runtime processes pending microtasks before moving on to the next task, which explains many surprising console-output questions in JavaScript interviews.
The example below deliberately combines synchronous functions, a Promise, queueMicrotask, setTimeout, and a closure. Read the console output in order and pay attention to when each callback becomes eligible to execute. The comments inside the program map each operation to the underlying JavaScript execution model.
console.log("1. Script execution starts");
// This function demonstrates ordinary synchronous execution.
function calculateTotal(price, quantity) {
console.log("2. calculateTotal() starts");
const total = price * quantity;
console.log(`3. Calculated total: ${total}`);
console.log("4. calculateTotal() finishes");
return total;
}
// A closure keeps access to the variable created in its outer scope.
function createCounter() {
let count = 0;
return function increment() {
count += 1;
console.log(`5. Closure counter: ${count}`);
return count;
};
}
console.log("6. Calling synchronous function");
const total = calculateTotal(100, 3);
console.log(`7. Returned value: ${total}`);
console.log("8. Creating closure");
const counter = createCounter();
counter();
counter();
// Promise callbacks are placed in the microtask queue.
Promise.resolve().then(() => {
console.log("9. Promise microtask executed");
});
// queueMicrotask() also schedules work in the microtask queue.
queueMicrotask(() => {
console.log("10. queueMicrotask callback executed");
});
// setTimeout schedules a task that runs after the current stack
// and pending microtasks have been processed.
setTimeout(() => {
console.log("11. setTimeout callback executed");
// A new microtask created inside a timer runs after this callback
// finishes and before another task gets its turn.
Promise.resolve().then(() => {
console.log("12. Microtask created inside setTimeout executed");
});
}, 0);
console.log("13. More synchronous code runs before async callbacks");
function demonstrateCallStack() {
console.log("14. demonstrateCallStack() entered");
function nestedFunction() {
console.log("15. nestedFunction() entered");
console.log("16. nestedFunction() finished");
}
nestedFunction();
console.log("17. demonstrateCallStack() finished");
}
demonstrateCallStack();
console.log("18. Main script finished");
// Expected high-level order:
// 1-8: synchronous execution happens immediately.
// Promise and queueMicrotask callbacks wait in the microtask queue.
// The timer waits in the task queue.
// Synchronous code from 13-18 runs before either asynchronous queue.
// Then microtasks 9 and 10 execute in scheduling order.
// Finally, the timer callback executes.
// The microtask created by the timer executes after that timer callback.
Conclusion
The key idea is that JavaScript does not randomly execute asynchronous code later. The engine executes synchronous code on the call stack, while the surrounding runtime coordinates asynchronous operations and places completed callbacks into queues. The event loop then helps move eligible work back toward execution when the stack is available.
Microtasks are particularly important because Promise handlers and queueMicrotask callbacks are processed before the runtime moves to the next task. This is why a Promise callback can execute before a zero-delay setTimeout callback even though the timer was registered first.
Once you understand the call stack, execution contexts, closures, microtask queue, task queue, and event loop, many JavaScript behaviors become predictable. These concepts are not just interview topics; they are fundamental to building responsive Node.js services and browser applications.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ full credit and traffic to the original publisher.