LoopLens

JavaScript execution context, explained

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.

Test yourself. When this is on, LoopLens pauses before every step that prints to the console and asks what it will print. Pick an answer to check it, or choose Just show me.Step 0 of 11 · virtual clock 0 ms
Event loop:Script (now)MicrotasksTask, then it repeats.The event loop runs the whole script first. Then it runs every microtask, then one task, and repeats.

Code (current line highlighted)

1current line: console.log(greet("Ada"));
2
3function greet(name) {
4 const message = "Hello, " + name;
5 return message;
6}
7
8try {
9 console.log(total);
10} catch (e) {
11 console.log(e.name);
12}
13let total = 10;
14console.log(total);

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.

main script

Web APIs (timers waiting in the browser)The browser counts timers outside JavaScript. When a timer finishes, its callback moves to the task queue.

empty
The whole script runs as ONE task on the call stack. Lines run top to bottom.

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.

empty

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.

empty

Console output

nothing yet

Scope and variables (yellow = changed this step)

no variables yet

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);
  1. Before line 1 runs, the global context is created. The function declaration greet is stored in full, so it can be called from the very first line. total is known too, but as a let it stays uninitialized.
  2. Line 1 calls greet("Ada"). A new function context goes on the call stack, with its own name and message.
  3. greet returns Hello, Ada, its context is removed, and the global code continues.
  4. Reading total before its let line throws a ReferenceError. The catch block prints its name: ReferenceError.
  5. The line let total = 10 runs, so total finally 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 var variable with the value undefined;
  • registers let, const and class names 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

DeclarationUsed before its line
function f() {}Works: the whole function is ready.
var x = 1x is undefined.
let x = 1, const x = 1ReferenceError (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); // 1

Note: 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(): this is obj.
  • Plain call fn(): this is undefined in strict mode (and in modules and classes), or the global object in old non-strict scripts.
  • Constructor call new Fn(): this is the new object.
  • Arrow functions have no this of their own; they use the this of 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 this to 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.

Next: How the JavaScript call stack works →

Open the visualizer with your own code