JavaScript Runtime Model
JavaScript execution with the call stack, heap, event loop, and async tasks.
JavaScript works by giving a host environment, usually a browser or Node.js, a dynamic language runtime with one main execution thread, a heap for objects, a call stack for active functions, and an event loop for scheduling work that completes later. That simple description explains most of the language's behaviour, including why synchronous code blocks the page, why promises do not run in parallel by themselves, and why memory leaks happen when objects stay reachable longer than intended.
The engine and the execution model
A JavaScript engine reads source code, parses it into an abstract syntax tree, and turns that into executable instructions. Modern engines do not simply interpret each line forever. They use a mix of parsing, bytecode, and just in time optimisation for hot code paths. If a function runs often with predictable types, the engine may generate faster machine code. If those assumptions break, it can deoptimise and fall back.
At runtime, function calls push frames onto the call stack. Local variables and control flow for the current function live there. Objects, arrays, and closures live on the heap. When the stack is empty, the runtime can pull the next task from a queue and continue. This is why long running loops freeze interfaces. While a frame is on the stack, nothing else on that thread can render, process user input, or run another callback.
Why asynchronous code exists
Browsers and Node.js both need to wait on work that is slower than CPU execution, such as timers, network I/O, file reads, or user input. JavaScript itself does not perform that waiting in the stack. The host environment does. When code starts a fetch or sets a timeout, the host registers the operation. Once it completes, a callback or promise continuation is queued for later execution.
The event loop decides when queued work runs. Microtasks, such as promise reactions, run before the runtime moves on to the next macrotask, such as a timer or I/O callback. That ordering explains a common surprise: a resolved promise callback often runs before a setTimeout(..., 0) callback even though the timeout looks immediate.
Objects, closures, and garbage collection
JavaScript is garbage collected. The engine reclaims heap memory that is no longer reachable from live roots such as globals, stack frames, or event listener references. The key word is reachable, not useful. An object can be useless to the product and still remain in memory if something still references it. That is why forgotten timers, DOM listeners, caches with no eviction, or closures that capture large objects can leak memory.
Closures are especially important. A function keeps access to variables from the scope where it was created, even after that outer function returns. This makes modules, callbacks, and private state straightforward, but it also extends object lifetime in ways new developers often miss.
What the language is good at
JavaScript is effective because it is embedded everywhere the web runs, works well with event driven interfaces, and can evolve without recompiling native binaries for every user. Its weakness is that dynamic flexibility can obscure errors until runtime. The language works best when teams lean on modules, type checking, clear async boundaries, and disciplined memory ownership. Under the surface, JavaScript is less mysterious than it first appears: one stack, one heap, queued work, and an engine constantly trading flexibility for speed.