Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 4 – Functions, scope and closures

Scope, the scope chain and execution contexts

Trace how JavaScript resolves names through block, function, module and global scope, see execution contexts and the call stack, and avoid accidental globals.

  • Intermediate
  • 22 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 22 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Trace how a name is resolved through block, function, module and global scope
  • Explain execution contexts, the call stack and environment records at a high level
  • Avoid accidental globals and shadowing bugs
  • Predict which top-level declarations classic scripts share and modules keep to themselves

Before you start

What you’ll learn · Before you start

What you will learn

  • Trace how a name is resolved through block, function, module and global scope
  • Explain execution contexts, the call stack and environment records at a high level
  • Avoid accidental globals and shadowing bugs
  • Predict which top-level declarations classic scripts share and modules keep to themselves

Before you start

On this page

Scope is the part of a program in which a name can be used. JavaScript decides it lexically: from where the code is written, not from where or how a function is called. Blocks, function bodies, modules and the global level each hold their own names, and they nest like boxes inside boxes. To find a name, the engine looks in the innermost scope first and then in each enclosing one, out to the global level; that path is the scope chain, and a name found nowhere is a ReferenceError. While the program runs, the engine also keeps track of what is running with execution contexts: each function call gets one, holding that call’s variables, and the contexts of the calls that have not finished yet form the call stack. The two chains are easy to confuse. This lesson shows both in programs that run, and the accidental globals and shadowing bugs they explain.

Four kinds of scope

  • Block scope. A pair of braces, { … }, as in an if, a loop or a bare block, holds the let, const and class declarations written in it. In strict code, which includes every module, a function declared in a block belongs to that block too.
  • Function scope. Each call of a function has its parameters, its var declarations and everything declared at the top of its body.
  • Module scope. The top level of an ES module holds its imports and top-level declarations. Other modules see only what it exports.
  • Global scope. Outside every function and module sit the built-ins (console, Math, globalThis) and, in classic scripts, the scripts’ top-level declarations.

var is the exception to block scope: it ignores blocks and belongs to the whole function, or to the global object at the top of a classic script. Modern code uses let and const; the next lesson of this module looks at var and the rules for using a name before its declaration.

The scope chain: looking a name up

Here one name, level, is declared four times at four levels. Each line prints the declaration that a use of the name found:

One name in four scopes JavaScript · scope-chain.mjs
// One name, four scopes. Each line says which declaration the name level resolved to.
const level = 'module';

function outer() {
  const level = 'function outer';

  if (level !== '') {
    const level = 'block inside outer';
    console.log(`in the block: ${level}`);
  }

  function inner() {
    console.log(`in inner: ${level}`); // inner has no level of its own: the next scope out is outer's
  }
  inner();

  console.log(`in outer: ${level}`);
}

outer();
console.log(`at the top of the module: ${level}`);

// A name that no scope declares is a ReferenceError, not undefined.
try {
  // eslint-disable-next-line no-undef -- the misspelt name is the point
  console.log(levl);
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

Output

in the block: block inside outer
in inner: function outer
in outer: function outer
at the top of the module: module
ReferenceError: levl is not defined

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node scope-chain.mjs

Version note

When you press Run, QuickJS quotes the missing name: ReferenceError: 'levl' is not defined.

In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:

in the block: block inside outer
in inner: function outer
in outer: function outer
at the top of the module: module
ReferenceError: 'levl' is not defined
  • Inside the if block, the nearest declaration is the block’s own, so that one wins. The condition level !== '' sits outside the braces, so it still reads outer’s level.
  • inner declares no level, so the lookup moves out one scope, to outer’s function scope, and stops at the first match. It never reaches the module’s level.
  • After the block has ended, its level is gone, and outer sees its own again.
  • At the top of the module, only the module’s declaration is in reach. Inner scopes can see out; outer scopes cannot see in.

The last lines show what happens when the chain runs out: the misspelt levl is declared nowhere, and reading it throws a ReferenceError. That is a feature. A typo that silently gave undefined would surface much later, far from the line with the mistake.

Scope is lexical, not decided by the caller

Where a function is called makes no difference to the names it sees. What counts is where its code sits:

A function called from another scope keeps its own JavaScript · lexical-scope.mjs
// Scope comes from where a function is written, not from where it is called.
const rate = 18;

function withTax(pricePaise) {
  return Math.round(pricePaise * (1 + rate / 100)); // this rate is the module's, always
}

function quoteForExport(pricePaise) {
  const rate = 0; // a different rate, local to this function
  return `export quote: ${withTax(pricePaise)} (local rate ${rate})`;
}

console.log(withTax(10000));
console.log(quoteForExport(10000));

// A function created inside another sees that function's variables, even when it is called elsewhere.
function makeGreeter(greeting) {
  return function greet(name) {
    return `${greeting}, ${name}`;
  };
}
const greetHindi = makeGreeter('Namaste');
const greeting = 'Hello'; // does not affect greetHindi
console.log(greetHindi('Asha'), '|', greeting);

Output

11800
export quote: 11800 (local rate 0)
Namaste, Asha | Hello

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node lexical-scope.mjs

withTax() is written at the top of the module, so rate inside it means the module’s rate, 18, even when quoteForExport() calls it from inside a function that has a rate of its own. If JavaScript looked names up through the callers instead (dynamic scope, which some other languages use), the export quote would have used 0.

The second half shows the same rule from the other side: greet() is written inside makeGreeter(), so it keeps seeing that call’s greeting, 'Namaste', after makeGreeter() has returned and even though the module later declares its own greeting. A function together with the names it can reach this way is a closure, the subject of a later lesson of this module.

Execution contexts and the call stack

The specification describes running code with execution contexts. One is created for the module or script when it starts and one for every function call; each tracks the code being run and the names it can use. The contexts sit on the execution context stack: a call pushes a new context, which becomes the running execution context, and a return pops it, so the caller’s context is running again. Debuggers and stack traces show the same stack, as frames. This program prints the stack, as V8 reports it, at four moments:

The call stack at four moments of a checkout JavaScript · call-stack.mjs
// The call stack while nested calls run: each line lists the running function first, then its callers.
// The names come from V8's stack trace (error.stack is not standard, so its format differs between engines).

function stack() {
  return new Error().stack
    .split('\n')
    .slice(2) // drop the "Error" line and the frame of stack() itself
    .map((line) => line.trim().match(/^at (\S+) \(/)?.[1] ?? 'module code')
    .filter((name, i, all) => all.indexOf('module code') === -1 || i <= all.indexOf('module code'))
    .join(' <- ');
}

function applyCoupon(totalPaise) {
  console.log(`3. ${stack()}`);
  return totalPaise - 5000;
}

function priceCart(items) {
  console.log(`2. ${stack()}`);
  const total = items.reduce((sum, item) => sum + item, 0);
  return applyCoupon(total);
}

function checkout(items) {
  console.log(`1. ${stack()}`);
  const due = priceCart(items);
  console.log(`4. back in checkout: ${stack()}`);
  return due;
}

console.log(`due: ${checkout([40000, 25000])}`);

Output

1. checkout <- module code
2. priceCart <- checkout <- module code
3. applyCoupon <- priceCart <- checkout <- module code
4. back in checkout: checkout <- module code
due: 60000

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node call-stack.mjs

At step 3 the stack holds four contexts: applyCoupon is running, priceCart and checkout are waiting for the calls they made, and the module’s code is waiting at the bottom. By step 4, priceCart and applyCoupon have returned and their contexts are gone, with their local variables. The reduce() callback ran in between and left no trace either: a context exists only while its call runs. The example has no Run button because it reads V8’s error.stack, which is not standard; QuickJS reports the same calls in its own format.

The call stack and the scope chain are different chains, and this is the point that makes scope click. At step 3, the call stack runs through priceCart and checkout, but applyCoupon cannot see either function’s variables, because it was not written inside them:

While applyCoupon runs, the call stack goes through priceCart and checkout, but its scope chain goes straight to the module.Call stackScope chainof applyCouponapplyCoupon(running)priceCartcheckoutmodule codeapplyCoupon'senvironment:totalPaisemoduleenvironment:checkout,priceCart,applyCoupon,stackglobalenvironment:console, Math …called bycalled bycalled byouterouter

The call stack and the scope chain are different chains

Text description of the diagram

The diagram shows two chains at the same moment of `call-stack.mjs`, while `applyCoupon` runs.

  • The call stack, on one side, records who called whom: `applyCoupon` is running, it was called by `priceCart`, which was called by `checkout`, which was called by the module code.
  • The scope chain of `applyCoupon`, on the other side, records where names are looked up. It starts with the environment of the running call, which holds the parameter `totalPaise`. Its outer environment is the module's, which holds `checkout`, `priceCart`, `applyCoupon` and `stack`, because `applyCoupon` is written at the top of the module. The module's outer environment is the global one, with built-ins such as `console` and `Math`.
  • `priceCart` and `checkout` are on the call stack but not on the scope chain: `applyCoupon` cannot see their local variables, however it was called.

Environment records, at a high level

In the specification, every scope is an environment record: a table of the names (the bindings) that one block, function call, module or the global level declares. Each record has an outer reference to the record around it, and those references are the scope chain. Looking a name up means checking the running context’s record, then its outer record, and so on, until the global record; a miss there is the ReferenceError.

Two details explain the examples above:

  • A function object remembers the environment record it was created in. Each call creates a new function environment record for its parameters and variables, and that record’s outer reference is the remembered one, not the caller’s. That is lexical scope, written as a rule.
  • A new record is created each time its code runs: each call of a function, and each pass through a block, gets its own set of bindings. Two calls of the same function never share their local variables.

The specification notes that environment records are a way to describe the language and need not exist inside an engine as such; engines may store variables however they like, as long as programs behave as described.

Scripts and modules at the top level

A browser runs each <script> tag without type="module" as a classic script, and all classic scripts of a page share one global scope. The global environment has two halves: the global object (window in a browser, globalThis everywhere), which holds the built-ins and the top-level var and function declarations of scripts, and a separate table for the scripts’ top-level let, const and class. Node.js’s vm module runs code as classic scripts, which shows the rules without a browser:

Three classic scripts and a module JavaScript · two-scripts.mjs
// Classic scripts share one global scope. node:vm runs each string below as a classic script, the way a browser runs
// each <script> tag without type="module".
import { runInThisContext } from 'node:vm';

runInThisContext('var cart = 3; let user = "asha"; function total() { return cart * 2; }');
console.log(`globalThis has cart: ${'cart' in globalThis}, user: ${'user' in globalThis}, total: ${'total' in globalThis}`);

// A second script sees the first script's let, even though it is not a property of globalThis.
console.log(`second script reads: ${runInThisContext('`${user} ${total()}`')}`);

// A third script that declares the same name again fails before any of its code runs.
try {
  runInThisContext('console.log("third script starts"); let user = "ravi";');
} catch (error) {
  console.log(`${error.name}: ${error.message}`);
}

// The module's own top-level names are invisible to scripts.
const moduleOnly = 'module';
console.log(`the module sees ${moduleOnly}; a script sees moduleOnly as: ${runInThisContext('typeof moduleOnly')}`);

Output

globalThis has cart: true, user: false, total: true
second script reads: asha 6
SyntaxError: Identifier 'user' has already been declared
the module sees module; a script sees moduleOnly as: undefined

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node two-scripts.mjs

  • The first script’s var cart and function total became properties of globalThis; its let user did not.
  • A second script still sees user and calls total(): top-level declarations of classic scripts are shared by every script that runs later.
  • A third script that declares user again fails before any of its code runs, so its first line never prints: before running a script, the engine checks all of its top-level declarations against the names that already exist. Two third-party snippets on one page that both use let config collide this way.
  • The module’s own moduleOnly is invisible to scripts. Each module has its own module scope, so modules never collide like that, and at the top of a module this is undefined.

The example has no Run button because QuickJS has no node:vm.

Accidental globals

In sloppy mode, the old default of classic scripts, assigning to a name that no scope declares does not fail: it creates a property on the global object. Strict mode turns that into a ReferenceError:

Assigning to an undeclared name, three ways JavaScript · accidental-global.mjs
// Assigning to a name that no scope declares. Indirect eval runs its code the way a classic script runs: at the
// global level and, without "use strict", in sloppy mode.
// eslint-disable-next-line no-eval -- indirect eval runs each string as a classic script would run
const runAsScript = eval;

runAsScript('function addVisit() { visits = 1; } addVisit();');
console.log(`sloppy script: visits became a global property: ${'visits' in globalThis}, value ${globalThis.visits}`);

try {
  runAsScript('"use strict"; function addSale() { sales = 1; } addSale();');
} catch (error) {
  console.log(`strict script: ${error.name}: ${error.message}`);
}

// Module code is always strict, so the same mistake fails at once.
function addRefund() {
  // eslint-disable-next-line no-undef -- the undeclared name is the mistake this line shows
  refunds = 1;
}
try {
  addRefund();
} catch (error) {
  console.log(`module: ${error.name}: ${error.message}`);
}

Output

sloppy script: visits became a global property: true, value 1
strict script: ReferenceError: sales is not defined
module: ReferenceError: refunds is not defined

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node accidental-global.mjs

Version note

The QuickJS engine behind the Run button prints both errors with the name in quotes, for example ReferenceError: 'sales' is not defined.

In your browser, QuickJS (quickjs-emscripten 0.32.0) prints:

sloppy script: visits became a global property: true, value 1
strict script: ReferenceError: 'sales' is not defined
module: ReferenceError: 'refunds' is not defined

Indirect eval, const runAsScript = eval; runAsScript(code), runs code at the global level and in sloppy mode unless the code asks for strict mode, much like a classic script (only its let and const stay inside the eval). In the first, sloppy, script, visits = 1 quietly created globalThis.visits: any other code could read or overwrite it, and a typo in a variable name becomes a new global instead of an error. The second script starts with "use strict", and the same assignment throws. Module code is always strict, so addRefund() fails at the first call. Modules, classes and "use strict" all prevent accidental globals; ESLint’s no-undef rule, part of its recommended set, reports them before the code runs.

Shadowing

A declaration in an inner scope with the same name as one outside shadows it: inside the inner scope, the name means the inner declaration. That is legal, and sometimes useful, but it hides bugs:

A shadowed variable and a shadowed name used too early JavaScript · shadowing.mjs
/* eslint-disable prefer-const, @typescript-eslint/no-unused-vars -- these rules report the shadowing bugs this file shows */
// Shadowing: an inner declaration with the same name hides the outer one for the rest of its scope.
function longestName(names) {
  let longest = '';
  for (const name of names) {
    if (name.length > longest.length) {
      let longest = name; // a new variable for this block only: the outer longest never changes
    }
  }
  return longest;
}

function longestNameFixed(names) {
  let longest = '';
  for (const name of names) {
    if (name.length > longest.length) {
      longest = name; // no let: this updates the outer variable
    }
  }
  return longest;
}

const names = ['Ravi', 'Meenakshi', 'Asha'];
console.log(JSON.stringify(longestName(names)), JSON.stringify(longestNameFixed(names)));

// An inner declaration shadows the outer name from the start of its block, even on lines above it.
const discountPercent = 10;
function finalPrice(pricePaise) {
  try {
    const half = discountPercent / 2; // reads the inner discountPercent, which is not initialised yet
    const discountPercent = half;
    return pricePaise - (pricePaise * discountPercent) / 100;
  } catch (error) {
    return error.name;
  }
}
console.log(finalPrice(10000));

Output

"" "Meenakshi"
ReferenceError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node shadowing.mjs

longestName() returns "" for every list. The let longest = name; inside the if declares a new longest that lives only in that block, so the outer longest is never updated. Dropping the let, as in longestNameFixed(), assigns to the outer variable.

The second function reads discountPercent on a line above the inner declaration, and still gets the inner one: a declaration shadows the outer name from the start of its block, not from its own line. Until its line runs, the inner name is uninitialised, so the read throws a ReferenceError instead of falling back to the outer 10. Renaming the inner variable, here to something like halfDiscount, fixes both kinds of shadowing bug. ESLint’s no-shadow rule reports every shadowing declaration; it is not in the recommended set, so a project turns it on deliberately. Even without it, no-unused-vars notices that the inner longest is never read and prefer-const that the outer one never changes, which is why the example switches both rules off in its first line.

JavaScript & TypeScript Online Compiler Paste scope-chain.mjs, rename one level, and predict each line before you run it.

Interview questions

Fresher · What does lexical scope mean in JavaScript?

Lexical scope means that the names a piece of code can use are decided by where the code is written in the source, not by where or how it is called. Blocks, functions, modules and the global level nest, and a name is looked up from the innermost scope outwards along that nesting; this path is the scope chain, and a name declared nowhere throws a ReferenceError. So a function written at the top of a module sees the module’s variables even when another function, with variables of the same names, calls it. A function created inside another function keeps seeing that outer call’s variables after it returns, which is what a closure is. The call stack is a separate chain: it records who called whom, and it plays no part in finding names.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A page loads analytics.js and then app.js as classic scripts. After analytics.js is updated, app.js stops with "SyntaxError: Identifier 'config' has already been declared", and none of its lines run. Explain why, why var in both files would have hidden the problem, and how you fix it for good.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    In a classic browser script, var name = { first: 'Asha' }; console.log(name.first); prints undefined. The same two lines in a module, or in Node.js, print Asha. Explain the difference, and what it tells you about top-level declarations in classic scripts.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Scope is where a name can be used. let, const and class are block-scoped; parameters and var belong to the function; a module has its own top-level scope; the global scope holds the built-ins.
  • A name is looked up from the innermost scope outwards, and the first match wins. A name found nowhere is a ReferenceError.
  • Scope is lexical: a function sees the names around the place where it was written, never its caller’s.
  • Each call creates an execution context; the stack of unfinished contexts is the call stack. It records who called whom, which is a different chain from the scope chain.
  • Classic scripts share one global scope: var and functions become globalThis properties, and a second top-level let of the same name is a SyntaxError. Modules keep their names to themselves.
  • Strict code, which includes every module, turns an assignment to an undeclared name into a ReferenceError. Shadowed names hide updates and apply from the start of their block; rename them, and let no-undef and no-shadow find the rest.

Exercise

Exercise · Easy · JavaScript

Fix an accidental global and a shadowed variable

tally.mjs came from an old classic script and has two scope bugs. Fix both without changing what the functions are meant to do.

  • countWords(text) returns an object that maps each word, in lower case, to the number of times it appears: countWords('The cat saw the hat') is { the: 2, cat: 1, saw: 1, hat: 1 }. In the old script it also created a global variable by accident; in a module, which is strict, the same line throws a ReferenceError. Nothing may be left on globalThis.
  • longestWord(text) returns the longest word of the text, the first one when several are equally long, and '' for a text with no words. It returns '' for every text, because an inner declaration hides the variable it should update.

Run the sample tests when you are done.

Starter code · tally.mjs

// Two helpers from an old classic script (the exercise of lesson javascript/scope-and-execution-contexts). Each one
// has a scope bug.
/* eslint-disable no-undef, prefer-const, @typescript-eslint/no-unused-vars -- the two bugs, which these rules report */

export function countWords(text) {
  counts = {};
  for (const word of text.toLowerCase().split(/\s+/).filter(Boolean)) {
    counts[word] = (counts[word] ?? 0) + 1;
  }
  return counts;
}

export function longestWord(text) {
  let longest = '';
  for (const word of text.split(/\s+/)) {
    if (word.length > longest.length) {
      let longest = word;
    }
  }
  return longest;
}
The sample tests · tally.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { countWords, longestWord } from './tally.mjs';

test('countWords counts each word in lower case', () => {
  assert.deepEqual(countWords('The cat saw the hat'), { the: 2, cat: 1, saw: 1, hat: 1 });
});

test('countWords leaves nothing on globalThis', () => {
  countWords('one two one');
  assert.equal('counts' in globalThis, false);
});

test('countWords of an empty text is an empty object', () => {
  assert.deepEqual(countWords('   '), {});
});

test('longestWord finds the longest word', () => {
  assert.equal(longestWord('a quick brown fox'), 'quick');
});

test('longestWord keeps the first of equally long words, and handles no words', () => {
  assert.equal(longestWord('red tan sky'), 'red');
  assert.equal(longestWord(''), '');
});
A hint

Look at the first line inside countWords(): the name it assigns is declared nowhere, so give it a const or let. In longestWord(), the let inside the if block creates a second longest that exists only in that block; drop the keyword so that the assignment updates the outer variable.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does this module print?

    Read the code, then choose one answer.

    const city = 'Pune';
    
    function where() {
      return city;
    }
    
    function visit() {
      const city = 'Kochi';
      return where();
    }
    
    console.log(visit());
    Show the answer to question 1

    Answer: Pune

    where() is written at the top of the module, so its city is the module's, whoever calls it. JavaScript scope is lexical: the city inside visit() is not on where()'s scope chain.

  2. Question 2 of 6 What does this module print?

    Read the code, then choose one answer.

    let count = 1;
    {
      let count = 2;
      console.log(count);
    }
    console.log(count);
    Show the answer to question 2

    Answer: 2, then 1

    The block declares its own count, which shadows the outer one inside the braces. After the block ends, its count is gone and the outer one, still 1, is the only one in reach. Declaring the same name in a nested scope is allowed; only the same scope may not declare it twice.

  3. Question 3 of 6 What happens when check() runs?

    Read the code, then choose one answer.

    const limit = 5;
    
    function check() {
      console.log(limit);
      const limit = 10;
    }
    
    check();
    Show the answer to question 3

    Answer: It throws a ReferenceError

    The inner const limit shadows the outer one from the start of the function body, not from its own line, and it is uninitialised until its line runs. The console.log on the line above reads it too early.

  4. Question 4 of 6 What does this module print?

    Read the code, then choose one answer.

    function kinds() {
      if (true) {
        var a = 'var';
        let b = 'let';
      }
      return `${typeof a} ${typeof b}`;
    }
    
    console.log(kinds());
    Show the answer to question 4

    Answer: string undefined

    var ignores blocks and belongs to the whole function, so a still holds its string after the if. let b exists only inside the block; outside it the name is undeclared, and typeof of an undeclared name is "undefined" rather than an error.

  5. Question 5 of 6 checkout() calls priceCart(), which calls applyCoupon(). All three are written at the top of the same module. Inside applyCoupon(), which variables can be read?

    Choose one answer.

    Show the answer to question 5

    Answer: Its own, and the module's and global names

    The callers are on the call stack, but not on the scope chain. applyCoupon() was written at the top of the module, so its outer scope is the module's, and from there the global scope.

  6. Question 6 of 6 What does scope-chain.mjs print?

    What does this program print? Choose one answer.

    // One name, four scopes. Each line says which declaration the name level resolved to.
    const level = 'module';
    
    function outer() {
      const level = 'function outer';
    
      if (level !== '') {
        const level = 'block inside outer';
        console.log(`in the block: ${level}`);
      }
    
      function inner() {
        console.log(`in inner: ${level}`); // inner has no level of its own: the next scope out is outer's
      }
      inner();
    
      console.log(`in outer: ${level}`);
    }
    
    outer();
    console.log(`at the top of the module: ${level}`);
    
    // A name that no scope declares is a ReferenceError, not undefined.
    try {
      // eslint-disable-next-line no-undef -- the misspelt name is the point
      console.log(levl);
    } catch (error) {
      console.log(`${error.name}: ${error.message}`);
    }
    Show the answer to question 6

    Answer: it prints

    in the block: block inside outer
    in inner: function outer
    in outer: function outer
    at the top of the module: module
    ReferenceError: levl is not defined

    Each use finds the nearest declaration outwards from where it is written. inner() has none of its own, so it finds outer's; the block's level is gone once the block ends; and a misspelt name declared nowhere throws a ReferenceError instead of giving undefined.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.