Runtime Engine, Callstack, Scope
Last updated
Was this helpful?
Last updated
Was this helpful?
In modern browsers and in other environments such as NodeJS, JavaScript is a Just In Time Compiled language, which is a mix between Ahead Of Time Compilation and Interpretation.
After parsing and analyzing the source code, this approach achieves both interpretation followed by execution the explicit source code, and generating an optimized low-level language (Bytecode) which can be later executed if a similar context appears in the original source code thread.
A process called Hoisting is also performed before execution. All declarations, for both variables and functions, are processed first during the compilation phase before any of the code is executed. The declaration code is moved, or hoisted, from where they appear in the flow of the code to the top of the scope and pushed to the heap. Only declarations are hoisted, while assignments or other executable logic is left in place. Functions declarations are hoisted first, then variable declarations.
Therefore a function can be called even if it is explicitly described further in the code and variables declared with var do not throw errors if referenced before being assigned any value.
Popular JS runtimes (engine and additional methods) are V8 (behind Chrome, recently Microsoft Edge and NodeJS), JavaScriptCore (Safari) and SpiderMonkey (Mozilla Firefox).
The JS engine is single-threaded indeed but inside a runtime, we can achieve asynchronous computing. The anatomy of the runtime is described below.
The Call Stack operates as a Last-In-First-Out (LIFO) structure. When a function is called, it’s pushed onto the stack; once it completes, it's popped off. This process repeats until all functions are executed.
Once a task requires external APIs or methods (such as DOM manipulation, setting a timeout, or making an external API request), it triggers an asynchronous operation. When the operation completes, its callback is pushed to the Callback Queue. These callbacks will be executed in the order they were added, but only after the Call Stack is completely cleared of all synchronous tasks.
The microtask queue is a special queue that is processed by the JavaScript engine before the callback queue. This means that microtasks, such as promises or mutation observers, are always executed before any tasks from the callback queue, even if the callback was scheduled first.
Even though the setTimeout
callback is initiated before the Promise
, the promise is placed in the microtask queue and runs before the timeout, which is in the callback queue.
When you make a fetch request, the actual network operation is asynchronous, and the callback that handles the response (e.g., when the request finishes) is sent to the callback queue.
However, the response from fetch
typically returns a promise. Once that promise is resolved (e.g., after processing the response), the .then()
or .catch()
handlers are sent to the microtask queue.
Thus, the initial fetch goes to the callback queue, and any promises yielded from it go to the microtask queue.
HTTP response promises are added to the microtask queue with the following aims:
It ensures that HTTP fetches are executed as soon as possible.
It prevents HTTP fetches from blocking the main thread.
It allows developers to write more efficient and responsive asynchronous code.
The Scope is the context in which values and expressions are "visible" or can be referenced. If a variable or other expression is not "in the current scope," then it is unavailable for use. Scopes can also be layered in a hierarchy, so that child scopes have access to parent scopes, but not vice versa.
Function parameters have local scope within that function's body.
Declaring variables with let
turns the scope to block scope, meaning it basically creates a clone (a mask) if that variable has already been declared in the parent scope. More details on the ES6 features.