Topic: Learning notes

What Happens When a JavaScript Function Runs? Execution Context and Local Memory

Trace JavaScript function calls, parameters, return values, and local variables with original examples, then see how closures preserve state.

If you call the same function twice, does a variable inside it pick up where the previous call left off? When I practiced this, I initially thought it did. Tracing the calls separately showed me that an ordinary local variable is created again for each call.

These notes grew out of studying JavaScript: The Hard Parts, v3. I’ll follow a function call through its parameters, execution context, and return value. The examples focus on ordinary synchronous functions, with a small closure example to show where the simple model needs more detail.

Start with a call, then trace where execution goes next. · Source: GIPHY

Defining, referencing, and calling a function

Start with this code:

function double(value) {
  const result = value * 2;
  return result;
}

const action = double;
const first = action(3);
const second = double(5);

console.log(first, second); // 6 10

function double(value) defines the function. const action = double gives action a reference to that function; the function body doesn’t run on that line.

Then action(3) and double(5) make two calls. The values 3 and 5 are arguments supplied by the caller. The value in the definition is the parameter that receives each value. Both names refer to the same function, but each call has its own execution state.

What is an execution context?

When an ordinary function is called, JavaScript creates an execution context for that call. It tracks the current state of execution, variable bindings, and other information needed to run the code.

The course uses two ideas as a learning model:

  • Execution flow: where the program is and when it enters another function or returns to its caller.
  • Local memory: the parameters and local variables for this call, such as value and result.

These are useful ways to reason about the code, not a claim that every engine stores information in two literal boxes. Execution doesn’t always just move to the next line: calls, conditions, and loops change the control flow. Calling a function doesn’t create a new operating-system thread.

Trace the two calls

Back to double: each call creates its own function execution context.

Call Parameter value Local variable result Return value
action(3) 3 6 6
double(5) 5 10 10

The first call makes action(3) evaluate to 6, so first receives 6. The second call starts with 5 as its own value, calculates 10, and returns it to second.

return gives a value back to the caller, but it doesn’t have to be assigned to a variable directly. For example:

function double(value) {
  return value * 2;
}

const result = double(4) + 1;
console.log(result); // 9

Here, double(4) evaluates to 8. The program adds 1, then assigns 9 to result. By contrast, console.log() displays a value; it doesn’t replace a function’s return.

What happens when one function calls another?

function inner(value) {
  return value * 2;
}

function outer(value) {
  const doubled = inner(value);
  return doubled + 1;
}

console.log(outer(3)); // 7
  1. The outer code calls outer(3) and execution enters outer.
  2. outer reaches inner(value) and enters inner, where value is 3.
  3. inner returns 6 and its call finishes. Execution resumes in outer, which assigns 6 to doubled.
  4. outer returns 7, and the outer console.log() displays it.

Think of this as a call stack: enter outer, then inner; when inner finishes, return to outer, and finally return to the outer code. When a function completes, its execution context leaves the stack and control returns to the caller.

Why don’t repeated calls automatically accumulate?

function step() {
  let count = 0;
  count += 1;
  return count;
}

console.log(step(), step()); // 1 1

The first call creates count, increases it from 0 to 1, and returns it. The second call executes let count = 0 again, so it also returns 1.

I initially pictured both calls sharing the same count and guessed 1 2. The key is that count is declared inside the function: each call has its own variable. Code outside the function can’t directly access it by the name count either.

Call it again, and that call gets its own local variable. · Source: GIPHY

The exception: a closure can retain an outer variable

Saying that every local variable must disappear when a function ends leaves out an important case. If a returned inner function still references an outer variable, it can keep using that variable:

function makeCounter() {
  let count = 0;

  return function increment() {
    count += 1;
    return count;
  };
}

const firstCounter = makeCounter();
console.log(firstCounter(), firstCounter()); // 1 2

const secondCounter = makeCounter();
console.log(secondCounter()); // 1

The increment function returned by makeCounter() uses a closure to retain access to the outer count. Calling firstCounter twice updates the same count. Calling makeCounter() again creates a separate count for secondCounter.

The outer function’s execution context has left the call stack, but a variable still referenced by a closure can remain available. The observed result doesn’t tell us exactly when garbage collection occurs. Closures deserve a closer look of their own; for now, they show why the basic rule about fresh local variables needs context.

Putting the model to work

When I read a function call now, I ask: Is this a reference to a function or a call? Which parameter receives each argument? What local variables belong to this call? Where does execution resume after it returns?

Tracing one call at a time, while checking for closures, makes it easier to keep state from different calls separate.

References