Runtime Engine, Callstack, Scope
Runtime Engine, Call Stack, Scope
JIT compilers
Modern JS engines (V8, JavaScriptCore, SpiderMonkey) parse your source into an AST, generate bytecode for a fast interpreter, then—based on runtime profiling—optimize hot paths with a JIT compiler. This mixed strategy balances quick startup with high peak performance.
Hoisting (what actually happens)
Before executing your code, the engine creates execution contexts and their environment records. During this compile/setup phase, declarations are registered:
Function declarations are hoisted with their full definition and can be called earlier in the file.
vardeclarations are hoisted and initialized toundefined(the assignment happens later).let/constdeclarations are hoisted too, but remain in the Temporal Dead Zone (TDZ) until their declaration line runs—accessing them earlier throws aReferenceError.Function expressions are not hoisted as callable functions; only the variable binding (e.g., with
var) is hoisted.
logToConsole(); // ✅ "Hello"
console.log(x); // ✅ undefined (var is hoisted, assigned later)
function logToConsole() {
console.log("Hello");
}
var x = 5;Common fix: don’t rely on hoisting for readability—declare before use.
JavaScript runtimes (quick refresher)
A runtime = engine + Web/Host APIs + queues + event loop. The engine is single-threaded, but the runtime exposes asynchronous primitives (timers, network, DOM events) that schedule work back to JS.

Execution context & the Call Stack
The Call Stack is LIFO: a function call pushes a frame; returning pops it.
“Blocking” synchronous code runs to completion before the runtime can process queued callbacks/microtasks.
// Inspect order with DevTools
debugger; // step to observe the stack frames
console.log("1");
setTimeout(function () {
console.log("2");
}, 3000);
console.log("3");
console.log("4");
console.log("5");
setTimeout(function () {
console.log("6");
}, 0);
console.log("7");
console.log("8");
console.log("9");Microtasks vs (macro)tasks
Microtask queue runs after the current stack frame, before any timer/IO callbacks: Promise reactions,
MutationObserver, queueMicrotask.Task/callback queue contains timers, IO, UI events, etc. Result: Promise handlers run before
setTimeout(fn, 0)callbacks.
console.log("Start");
setTimeout(function () {
console.log("Timeout callback");
}, 0);
Promise.resolve().then(function () {
console.log("Promise microtask");
});
console.log("End");
// Output:
// Start
// End
// Promise microtask
// Timeout callbackNote:
fetch()completes in the host environment and resolves a Promise; its.then/.catchhandlers run as microtasks.
In the JS runtime, the microtask queue has higher priority and is drained immediately after the current call stack (and before rendering), while the callback (task/macro-task) queue runs after all pending microtasks and typically yields to rendering between tasks; examples of microtasks include Promise reactions (promise.then/catch/finally), queueMicrotask, and MutationObserver, so a network request with fetch resolves to a Promise whose handlers execute as microtasks, often ahead of timers; by contrast, the callback queue holds tasks like setTimeout/setInterval, MessageChannel/postMessage, user DOM events (e.g., click, input, submit), and certain network/DOM load events like img.onload or XMLHttpRequest.onload, which will run only after the microtask queue has been fully emptied.
Scope & the Scope Chain
Scope is where identifiers are visible. Child scopes can read parent bindings (via the scope chain), not vice-versa. JavaScript uses lexical (static) scoping: visibility is determined by where code is written.
var x = "declared outside function";
exampleFunction();
function exampleFunction() {
console.log("Inside function");
console.log(x); // reads from outer scope
}
Hoisting meets scope
This classic example shows shadowing + var hoisting inside a function scope: the inner var favouriteFood is hoisted (set to undefined) and shadows the outer one until assignment.
var favouriteFood = "grapes";
var foodThoughts = function () {
console.log("Original favourite food: " + favouriteFood);
var favouriteFood = "sushi";
console.log("New favourite food: " + favouriteFood);
};
foodThoughts();
// Output:
// Original favourite food: undefined <-- inner var is hoisted but uninitialized
// New favourite food: sushiAdditional notes:
Function parameters are local to the function body.
Block scope (
let/const) confines bindings to{ ... }blocks and prevents use before initialization (TDZ). Preferconst, useletonly when you must reassign; avoidvarin modern code.
Quick takeaways
Don’t depend on hoisting for clarity—declare before use.
Understand the event loop order: stack → microtasks → tasks.
Scope is lexical; inner scopes shadow outer bindings. Keep variable lifetimes tight (block scope) to reduce bugs.
Last updated
Was this helpful?