The Execution Context in JavaScript

Abstract image used to represent The Execution Context in JavaScript
Image by Robert Katzki.

An execution context is the runtime record JavaScript creates whilst evaluating code. It contains the relevant bindings and scope relationship and determines how this is resolved. That does not mean every context owns an ordinary dynamic this value: arrow functions resolve this lexically from their surrounding context.


What is an Execution Context?

Every time JavaScript runs a script or calls a function, it creates an execution context. This context defines how the code runs and how variables, functions, and objects are handled.

Think of it as a workspace where JavaScript keeps track of what it's currently doing. Execution contexts follow a structured lifecycle and work with the JavaScript execution stack, which manages function calls and variable scopes. Understanding this can be very helpful, especially when it comes to debugging tricky issues like unexpected variable values or function execution order, and doubly so when it comes to doing so in complex applications.


The Types of Execution Contexts

There are three main types of execution contexts in JavaScript:

1. Global Execution Context

This is the default execution context created when JavaScript first runs a script. It represents the outermost environment where global variables and functions exist.

We can illustrate this by consoling out this:

console.log(this);

In a browser classic script, toplevel this is normally window; in a browser module it is undefined. In Node.js, globalThis exposes the global object, but toplevel bindings and this also depend on whether the file is CommonJS or an ES module.

2. Function Execution Context

Every time a function is called, JavaScript creates a new execution context specifically for that function. This execution context contains:

  • The function's arguments and local variables.
  • Access to the outer lexical environment; this does not require the parent call to remain on the stack.

The typed examples in this article use TypeScript, which compiles to JavaScript. Here is a function with a typed parameter:

function greet(name: string) {
  console.log(`Hello, ${name}`);
}
greet("Maddie");

This example illustrates how JavaScript creates a new execution context every time a function is called. When greet("Maddie") is executed:

  1. A new function execution context is created and pushed onto the call stack.
  2. Inside the function, name is assigned the value "Maddie", and console.log runs.
  3. Once execution completes, the function execution context is removed from the call stack, returning control to the global execution context.

Each call has its own parameters and local bindings whilst retaining access to its outer lexical environment. Deep recursion can exhaust the call stack; creating an execution context does not itself cause a memory leak, although a reachable closure can retain bindings after the call returns.

3. Eval Execution Context

eval() has solidly fallen out of favour over the past few years (with good reason), but it is still the third context and, therefore, should be discussed. Just enter this section knowing that there is virtually no reason you would ever use eval() in a production environment.

eval() is a JavaScript function which executes a string of code as if it were part of the script. It dynamically interprets and runs the provided string, evaluating expressions, executing functions, and defining variables at runtime.

In terms of execution context, each eval() function creates a separate execution context of its own. For example:

eval("console.log('Executing inside eval')");

Avoid passing untrusted text to eval(): it treats that text as executable code. Direct eval() can also make scope analysis and optimisation harder for an engine. The exact parsing, compilation and caching behaviour is implementationdependent; it is not safe to assume every call follows the same recompilation path.


The Execution Context Lifecycle

Each execution context follows a threephase lifecycle:

1. Creation Phase

When JavaScript encounters a new execution context (global or function), it performs the following steps:

  • Creates the Variable Environment:

    Allocates memory for variables and functions.
  • Establishes access to the relevant outer lexical environment.
  • Determines

    this Binding: Decides what this should refer to.

2. Execution Phase

  • JavaScript executes the code inside the execution context.
  • Variables get assigned values, and functions run.
  • The context remains active until execution is complete.

3. Destruction Phase

  • Once execution finishes, the execution context is removed from the call stack.

JavaScript still runs each job to completion. Host APIs such as setTimeout arrange future tasks, whilst promise reactions run as queued jobs; neither changes the executioncontext rules for the code being run.


The Call Stack and Execution Contexts

JavaScript is singlethreaded, which means that it executes one task at a time. Execution contexts are managed using the call stack, a data structure that follows Last In, First Out (LIFO). For example:

function first() {
  console.log("First function");
}
function second() {
  first();
  console.log("Second function");
}
second();

Call Stack Order:

When this is called:

  1. The global execution context is created.
  2. second() is called → A new execution context is pushed onto the stack.
  3. first() is called inside second() → Another execution context is pushed.
  4. first() finishes → Its execution context is popped off.
  5. second() finishes → Its execution context is popped off.

In this way, this process ensures orderly execution of JavaScript functions, and the output looks like this:

First function
Second function

Execution Contexts in React and Next.js

Function Components and Execution Context

In React, function components follow exactly the same execution context rules as regular JavaScript functions.

function Greeting({ name }: { name: string }) {
  console.log("Rendering Greeting component");
  return <h1>Hello, {name}!</h1>;
}

Being a function, this follows the Function Execution Context, with every new render creating a new execution context for the component.

Hooks and Execution

Hooks such as useState() and useEffect() belong at the top level of a function component or a custom Hook. React relies on their call order to match state and Effects between renders. For example:

function Counter() {
  const [count, setCount] = useState(0);

  function increment() {
    setCount(count + 1);
  }

  return <button onClick={increment}>Count: {count}</button>;
}

If hooks were called conditionally, the execution order could become inconsistent, leading to issues like lost state or unintended rerenders in React applications. This is a very common problem when debugging poorly performing applications or components that don't behave as expected.

Next.js API Routes and Execution Context

A Next.js API route is a serverside function. Each invocation creates a function execution context, but that says nothing by itself about the lifetime of the process or module containing it.

export default function handler(req, res) {
  console.log("New API request");
  res.status(200).json({ message: "Hello from Next.js API" });
}

Each handler call follows the ordinary Function Execution Context model. Requestlocal bindings end with the call unless retained, but modulelevel state may persist when the same server process or warm serverless instance handles another request. It must not be treated as durable storage because the host may replace that instance.


Execution Contexts in Node.js

Browsers and Node.js both provide eventdriven host APIs around JavaScript. Their scheduling details differ, but an ordinary JavaScript job runs to completion in both environments.

Asynchronous Execution

For example, Node.js schedules this timer callback for later, after the current synchronous code has finished:

console.log("Start");
setTimeout(() => console.log("Timeout Callback"), 0);
console.log("End");

Even though setTimeout is called first, it executes after synchronous code because of the event loop.

Execution Context in Modules

Node.js modules have module scope and are evaluated according to their module system. CommonJS wraps each module in a function; ES modules use module environment records. Their toplevel bindings are not automatically global, and an evaluated module can retain state for as long as that module instance remains loaded.

Take for example if we had two modules like this:

// module1.ts
export const message = "Hello from Module 1";

// main.ts
import { message } from "./module1";
console.log(message);

When this is run:

  • Module Scope and Evaluation

    :
    • Each module (module1.ts and main.ts) is evaluated as its own module with modulescoped bindings.
    • Toplevel module bindings do not become properties of the global object merely because the module was evaluated.
  • Exporting a Variable

    :
    • In module1.ts, the variable message is declared and exported.
  • Importing into Another Module

    :
    • In main.ts, message is imported from module1.ts.
  • Logging the Output

    :
    • When console.log(message); runs in main.ts, it outputs Hello from Module 1.

Wrapping Up

The execution context is fundamental to how JavaScript works, affecting everything from variable scope to asynchronous behaviour. Whether you're writing vanilla JavaScript, working with React components, or handling API requests in Next.js, understanding execution contexts will help you write clearer and more efficient code.

If that still feels a bit abstract, it helps to remember that these mechanics do not disappear inside frameworks. On the Nando’s UK & Ireland Replatform, the same underlying JavaScript rules still shaped how code behaved across the browser, Node.js, and a fairly involved Next.js application.

Key Takeaways

  • JavaScript creates an execution context for every script and function call.
  • Execution contexts are managed via the call stack.
  • The global execution context is created first, followed by function execution contexts.
  • React function components create a new execution context per render.
  • APIroute calls create function execution contexts, whilst module scope and host instance lifetime determine whether modulelevel state persists.
  • Understanding execution contexts helps with debugging and optimising JavaScript performance.

Each call creates an execution context with its bindings and scope relationships. Arrow functions resolve this lexically from their surrounding environment. The call stack records which context is running, whilst closures can retain lexical bindings after a call returns.


Looking for technical direction?

I support teams that need senior judgement on React, Next.js, headless CMS architecture, performance, migrations, and technical SEO.