Skip to main content

What is execution context?

Execution context - the internal environment the JavaScript engine creates every time it runs code, holding that code's variables, scope chain, and this binding.

Theory

TL;DR

  • Think of it as a chef's workstation: each function call gets its own workspace with ingredients (variables), a reference book (scope chain), and a name tag for who owns it (this).
  • Three types: global (one per program), function (one per call), eval (avoid it).
  • Two phases: creation (hoisting happens here) and execution (code runs line by line).
  • this changes per context. Arrow functions skip creating their own and inherit from the parent.
  • When you hit an unexpected undefined or wrong this, trace the context stack.

Quick example

javascript
console.log(a); // undefined - hoisted in global context's creation phase var a = 2; console.log(a); // 2 function greet() { // New function context pushed onto the call stack console.log(b); // undefined - hoisted within this context only var b = 'hello'; console.log(b); // 'hello' } greet();

Global context hoists a to undefined during creation. When greet() runs, a fresh function context is pushed onto the call stack with its own b. That context can reach a via the scope chain, but b lives only inside it.

Two phases every context goes through

The engine runs each context in two steps.

Creation phase: var declarations are hoisted and set to undefined. Full function declarations are hoisted as-is. The scope chain is built. this is resolved.

Execution phase: code runs line by line, values are assigned, functions are called.

This is why reading x before var x = 1 gives undefined instead of a ReferenceError. The variable was registered during creation, just without a value yet.

How execution context differs from scope

Scope describes what variables a piece of code can see. Execution context is the full package: the variable object (all var declarations and function declarations), the scope chain pointing outward to parent contexts, the this value, and the position on the call stack. Scope is one component of context, not the whole thing.

this across contexts

In a method call, this is the object before the dot. In a standalone function call, this is the global object (or undefined in strict mode). Arrow functions do not create their own context. They capture this from the enclosing context at definition time.

javascript
const obj = { name: 'World', hello: function() { console.log(this.name); // 'World' - method context, this = obj setTimeout(function() { console.log(this.name); // undefined - new global context }, 0); setTimeout(() => { console.log(this.name); // 'World' - arrow inherits this from hello }, 0); } }; obj.hello();

The regular function in setTimeout gets its own global context. The arrow function does not create one at all.

Common mistakes

1. Expecting var to be block-scoped

javascript
if (true) { var score = 100; } console.log(score); // 100 - var belongs to the function or global context

var is scoped to its execution context, not to {} blocks. Use let or const for block scoping via LexicalEnvironment.

2. Losing this in callbacks

javascript
class Timer { constructor() { this.count = 0; } start() { setInterval(function() { this.count++; // TypeError: this is undefined in strict mode }, 1000); } }

This is probably the most common this question in junior JavaScript interviews. The callback runs in a new context where this is not the Timer instance. Fix: use an arrow function in the callback, or bind this in the constructor.

3. Expecting eval to be isolated

javascript
var secret = 'admin'; eval('console.log(secret)'); // 'admin' - shares the calling context's scope

eval creates its own context but inherits the outer scope. It also blocks several V8 optimizations. Use new Function() if you need dynamic code in a genuinely isolated context.

4. Forgetting strict mode changes this

In non-strict mode, this in a standalone function call is the global object. In strict mode, it is undefined. Adding "use strict" to an existing file can silently break code that depended on this === window.

Real-world usage

  • React function components: each render creates a new function context. Hooks like useState capture state for that context via closures.
  • Express route handlers: each request runs in its own function context. req and res are local to that call.
  • setTimeout callbacks: they run in a new global context. Arrow functions are the standard fix for preserving the outer this.
  • Node.js modules: each file runs inside a wrapper function, so top-level var does not leak to the global object.

Follow-up questions

Q: What is the difference between the creation phase and the execution phase?
A: Creation phase hoists var declarations to undefined, hoists function declarations in full, and resolves this. Execution phase runs the code line by line and assigns values.

Q: How does the scope chain work across contexts?
A: Each context holds a reference to its outer context's variable environment. When a variable is not found locally, the engine walks up the chain until it reaches the global context.

Q: What is the difference between the call stack and the execution context stack?
A: They describe the same structure from two angles. The call stack tracks which functions are waiting to return. The execution context stack holds the full state of each frame: variables, scope chain, and this.

Q: How do arrow functions affect execution context?
A: Arrow functions do not create their own context. They inherit this and scope from the lexical parent at the time of definition.

Q: Why is this undefined in Node.js strict mode inside a function?
A: Strict mode does not assign the global object to this in standalone function calls. The value stays undefined until explicitly set via .call(), .apply(), or .bind().

Examples

Hoisting across global and function contexts

javascript
// Global context - 'name' hoisted to undefined in creation phase console.log(name); // undefined var name = 'Alice'; console.log(name); // 'Alice' function getGreeting() { // Function context - runs its own creation phase console.log(greeting); // undefined (hoisted inside this context) var greeting = 'Hello, ' + name; // 'name' found via scope chain console.log(greeting); // 'Hello, Alice' } getGreeting();

name is reachable inside getGreeting through the scope chain pointing to the global context. greeting is local to the function's context and does not exist outside it.

this binding in a React component

javascript
function UserProfile({ userId }) { // New execution context on every render const [user, setUser] = React.useState(null); React.useEffect(() => { // Arrow function - no own context, inherits from UserProfile fetch(`/api/user/${userId}`) // userId from this render's context .then(res => res.json()) .then(setUser); }, [userId]); return user ? <div>{user.name}</div> : <div>Loading...</div>; }

Each render of UserProfile creates a fresh execution context. The arrow function in useEffect closes over userId from that specific render. Leave userId out of the dependency array and the effect will hold onto a stale value from an old context.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?