An execution context is the environment JavaScript sets up to run a piece of code. There is one global context for your script, and a new function context every time a function is called. Each context holds the variables and functions that code can use, a link to the outer scope, and the value of this. Some of this is set up before the first line runs, which is why hoisting happens.
Try it
This example calls a function before its declaration and reads a let variable too early. Press Next step and watch the call stack and the Scope and variables panel.
Code (current line highlighted)
Call stack (top first)The function running right now is on top. Calling a function adds a frame; returning removes it. Only one frame runs at a time.
Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.
Microtask queue (next first)Promise callbacks (.then, code after await, queueMicrotask) wait here. Once the call stack is empty, all of them run before the next task.
Task queue (next first)Timer callbacks wait here. The event loop takes one task, runs it, then empties the microtask queue before taking the next.
Console output
Scope and variables (yellow = changed this step)
Timeline (click a block or use the arrow keys)Script, function calls and returnsawait and microtasksTimers and tasksConsole outputErrors
Step by step: what the example does
console.log(greet("Ada"));
function greet(name) {
const message = "Hello, " + name;
return message;
}
try {
console.log(total);
} catch (e) {
console.log(e.name);
}
let total = 10;
console.log(total);- Before line 1 runs, the global context is created. The function declaration
greetis stored in full, so it can be called from the very first line.totalis known too, but as aletit stays uninitialized. - Line 1 calls
greet("Ada"). A new function context goes on the call stack, with its ownnameandmessage. greetreturns Hello, Ada, its context is removed, and the global code continues.- Reading
totalbefore itsletline throws aReferenceError. Thecatchblock prints its name: ReferenceError. - The line
let total = 10runs, sototalfinally gets a value and the last line prints 10.
Two phases: creation, then execution
When a context starts, JavaScript first goes through the code to set it up (the creation phase) and only then runs it line by line (the execution phase). In the creation phase it:
- creates every function declaration in full;
- creates every
varvariable with the valueundefined; - registers
let,constandclassnames without a value; - links the context to its outer scope, the scope the function was written in;
- decides the value of
this(for function calls, from how the function is called).
Hoisting, declaration by declaration
| Declaration | Used before its line |
|---|---|
function f() {} | Works: the whole function is ready. |
var x = 1 | x is undefined. |
let x = 1, const x = 1 | ReferenceError (temporal dead zone). |
class C {} | ReferenceError (temporal dead zone). |
var f = function () {} | f is undefined, so f() throws a TypeError. |
console.log(x); // undefined: var x already exists
var x = 1;
console.log(x); // 1Note: LoopLens can't show var hoisting yet. If you paste this code into the visualizer, it reports a ReferenceError instead of printing undefined. Real JavaScript prints undefined, then 1.
The temporal dead zone
The time between the start of a scope and the line that declares a let, const or class is called the temporal dead zone (TDZ). The name exists, so it hides any outer variable with the same name, but reading it throws. This catches the bugs that var hides, where code silently works with undefined.
How this is decided
- Method call
obj.method():thisisobj. - Plain call
fn():thisisundefinedin strict mode (and in modules and classes), or the global object in old non-strict scripts. - Constructor call
new Fn():thisis the new object. - Arrow functions have no
thisof their own; they use thethisof the context they were written in.
The Classes and this example in the menu above shows this inside a class's methods.
Contexts, the call stack and closures
Execution contexts are what the call stack holds: the global context sits at the bottom, and each function call pushes a new one on top. When a function returns, its context leaves the stack, but its variables can live on if an inner function still refers to them. That is a closure.
Common mistakes
- "let and const aren't hoisted." They are registered early; they just can't be read before their line.
- Calling a function expression before its line. Only function declarations are hoisted in full.
- Expecting
thisto follow where a function is written. For regular functions it depends on how the function is called; only arrow functions take it from where they are written.
What LoopLens simulates
LoopLens creates a real scope for the script and for every call, hoists function declarations and enforces the temporal dead zone, so the example above behaves exactly like real JavaScript (it is checked against Node.js). It follows strict-mode rules for this. Not supported yet: var hoisting, this at the top level of a script, and the arguments object.