AlgoInsight

Free lesson · JavaScript Basic to Advance

Lexical Scope Revisited

Easy5 min read

Where a Function Can See

We covered scope in Module 3 — variables declared inside a function are local to that function; variables in outer scopes are accessible through the scope chain. This Topic revisits that material with more precision, because the way scope works in JavaScript is the foundation closures are built on.

The principle that matters most: scope is determined by where a function is written, not by where it's called from. This rule has a name — lexical scope — and it's one of the most important things to understand about JavaScript.

const name = "Olena";

function greet() {
  console.log(`Hello, ${name}`);
}

function elsewhere() {
  const name = "Nikhil";   // local 'name', shadows the outer one
  greet();   // which 'name' does greet see?
}

elsewhere();

If you guessed "Hello, Olena", you've internalized lexical scope. The greet function looks for name in the scope where it was written — the top level, where name is "Olena". It doesn't matter that elsewhere has its own local name. greet doesn't see elsewhere's variables. It only sees the variables visible from where it was defined.

If the answer surprised you, this Topic is for you. Lexical scope is the rule, and once you understand it precisely, closures stop feeling mysterious.

What "Lexical" Means

The word "lexical" comes from the Greek for "word" — it refers to the literal, written form of code. Lexical scope means the scope is determined by the position of the code in the source text, not by anything that happens at runtime.

When the JavaScript engine sees a function definition, it permanently associates that function with the scope where it was written. From that moment on, no matter where the function is called from — passed to another function, stored in an array, attached to an event handler — it remembers its original scope.

This is different from how some languages work. In dynamic scope (used by some older languages and some scripting environments), a function looks up variables in the scope of whoever called it. JavaScript doesn't do that. JavaScript uses lexical scope exclusively.

const message = "I am the outer message";

function speak() {
  console.log(message);
}

function caller() {
  const message = "I am the caller's message";   // doesn't affect speak()
  speak();
}

caller();

speak was written at the top level, where message is "I am the outer message". When caller calls speak, the call happens from inside caller's execution context — but speak doesn't care. It looks up message in its own lexical scope, which is the top level. The output is the outer message.

This is the entire mechanism behind lexical scope. Functions remember the scope they were written in, not the scope they're called from.

Interactive animation: Called from a loud place, born in a quiet one - the function reads the variable where it was written.

The Scope Chain at Runtime

When a function tries to read a variable, the engine follows the scope chain — a series of references from inner scopes to outer scopes, ultimately reaching the global scope.

const level0 = "top level";

function outer() {
  const level1 = "in outer";

  function middle() {
    const level2 = "in middle";

    function inner() {
      const level3 = "in inner";

      console.log(level3);   // found here
      console.log(level2);   // found one level up (middle)
      console.log(level1);   // found two levels up (outer)
      console.log(level0);   // found at the top
    }

    inner();
  }

  middle();
}

outer();

When inner runs and looks for level0, the engine searches: inner's environment (not found), middle's environment (not found), outer's environment (not found), global environment (found). The search direction is always outward, never inward.

graph TD
    A[inner function looks up a variable] --> B[Search inner's own environment]
    B -->|not found| C[Search middle's environment]
    C -->|not found| D[Search outer's environment]
    D -->|not found| E[Search global environment]
    E -->|not found| F[ReferenceError]
    B -->|found| G[Return value]
    C -->|found| G
    D -->|found| G
    E -->|found| G

    style G fill:#10b981,color:#fff
    style F fill:#dc2626,color:#fff
This diagram shows the scope chain search process. When a function reads a variable, the engine searches the function's own environment first. If not found, it searches the next outer environment, then the one outside that, walking outward through the chain of enclosing scopes. The search ends either when the variable is found and its value returned, or when the engine reaches the global environment without finding it, at which point a ReferenceError is thrown.

The chain itself is built at function-creation time. Each function has a pointer to the environment it was created in, and that environment has a pointer to its outer environment, and so on. The chain is determined by where the function was defined, not by who called it.

Interactive animation: Miss locally, walk outward - the lookup checks the innermost scope first, then each enclosing scope in turn.

Interactive animation: The counter that counts nothing - a local variable shadows the outer one, so the lookup never reaches it.

Lexical Scope and the Call Stack

This is where students often get confused, so I want to be precise. The call stack (Topic 6.1) tracks which functions are currently running. The scope chain tracks which variables are accessible from each function. They are not the same thing.

function defineGreeter() {
  const name = "Olena";

  return function greeter() {
    console.log(`Hello, ${name}`);
  };
}

const myGreeter = defineGreeter();
// At this point, defineGreeter has returned. Its call stack entry is gone.
// But the function it returned still has access to 'name' from its lexical scope.

myGreeter();   // "Hello, Olena"

When myGreeter() is called, the call stack looks like [global, myGreeter]. defineGreeter is not on the stack — it returned a long time ago. But myGreeter was defined inside defineGreeter, so its lexical scope still includes defineGreeter's variables.

This is exactly what makes closures possible. The lexical scope is preserved even after the function that created it has finished running. The call stack and the lexical scope chain are separate mechanisms operating on different timelines.

Functions Defined Where They're Used

Lexical scope has a practical consequence: functions defined in different places have access to different things. This is sometimes the source of bugs.

const outerName = "from outer";

function logName() {
  console.log(outerName);
}

function definer() {
  const innerName = "from inner";

  function localLog() {
    console.log(innerName);   // ✅ has access to innerName
  }

  return [logName, localLog];
}

const [extLogger, intLogger] = definer();

extLogger();   // "from outer"
intLogger();   // "from inner"

logName is defined at the top level. It can see outerName but not innerName. localLog is defined inside definer. It can see both — innerName from its enclosing function and outerName from the global scope.

Both functions can be returned from definer and called from anywhere. Their scope is determined by where they were written, not by where they're called.

Common Pitfalls

Warning: The lexical scope traps.

Assuming a function sees its caller's variables. It doesn't. A function only sees variables from its own lexical scope. The caller's variables are completely invisible. If a function needs a value from its caller, the caller must pass it as an argument.

Confusing scope with the call stack. When you call a function from inside another function, the called function has its own scope chain based on where it was defined. The fact that it was called from somewhere doesn't grant it access to that caller's variables.

Expecting var and block scope to behave the same. A var in a block leaks to the enclosing function. A let or const in the same block stays in the block. Knowing this and verbalizing it is what separates senior JavaScript developers from beginners.

Why This Matters in Interviews

Interview: Lexical scope is a foundational JavaScript concept, and it shows up in interviews through output-prediction questions. Code with nested functions, shadowed variables, and functions being called from unexpected places — interviewers use these to test whether you understand that scope is determined at definition time, not at call time.

The senior-level question: "Explain lexical scope. How is it different from dynamic scope?" The expected answer touches on definition-time vs call-time scope resolution, mentions that JavaScript uses lexical scope exclusively, and ideally connects it to closures.

Topic 6.7 — Interview Lens: Closures — covers the deeper treatment.

Try It

Exercise: Predict the output of this code. Then run it to check:

let value = "outside";

function reveal() {
  console.log(value);
}

function obscure() {
  let value = "inside obscure";
  reveal();
}

function reveal2() {
  let value = "inside reveal2";
  reveal();
}

reveal();
obscure();
reveal2();

All three calls invoke reveal(). Two of them are called from inside functions that have their own local value. What does each call log, and why?

The answer reveals exactly what lexical scope means in practice. Once you can predict this without hesitation, you've internalized the rule. The next Topic — closures — is a direct consequence of this rule.

Back to JavaScript Basic to Advance