Skip to content

RESOURCES / BLOG

What is Lexical Scope in JavaScript? 

Many JavaScript bugs come from variables not being where you think they are. If you have ever logged a value in a callback and seen something unexpected, or watched all your button handlers print the same index, you have met lexical scope.

I keep hearing that “JS has lexical scope” and that it explains closures, var vs let behavior, and those loop bugs where every click handler logs the same number. What is lexical scope in JavaScript, how does it affect variable access and closures, and what are the best practices to avoid surprises? Code examples would be great.

Lexical scope is the rule that determines where variables are accessible based on where they are written in the source code. JavaScript decides scope at parse time, not at runtime, using nested blocks and functions to form a scope chain. A function can read variables from its own scope and any outer scopes, but not from inner scopes it does not contain.

  • Scopes are created by functions and by blocks delimited with braces when using let or const.
  • Inner scopes can access variables defined in outer scopes. The reverse is not true.
  • Closures happen when a function remembers variables from the scope where it was defined.
const greeting = "Hi";

function outer() {
  const name = "Ava";
  function inner() {
    console.log(greeting, name); // "Hi Ava"
  }
  inner();
}

outer();
// inner(); // ReferenceError - inner is not in this scopeCode language: JavaScript (javascript)

var is function-scoped. let and const are block-scoped. This difference explains the infamous loop-handler bug:

// Problem: var leaks one binding across the loop
const buttons = document.querySelectorAll("button");
for (var i = 0; i < buttons.length; i++) {
  buttons[i].addEventListener("click", () => console.log(i));
}
// All handlers print buttons.length

// Fix 1: use let to create a new binding per iteration
for (let j = 0; j < buttons.length; j++) {
  buttons[j].addEventListener("click", () => console.log(j));
}

// Fix 2: IIFE creates a new scope for each i
for (var k = 0; k < buttons.length; k++) {
  ((n) => buttons[n].addEventListener("click", () => console.log(n)))(k);
}Code language: JavaScript (javascript)

A closure is a function bundled with references to its surrounding scope. The closed-over variables continue to live as long as the function does.

function makeCounter(start = 0) {
  let count = start;        // captured by the inner function
  return () => ++count;     // closure
}

const next = makeCounter(10);
console.log(next()); // 11
console.log(next()); // 12Code language: JavaScript (javascript)
  • Function declarations and var are hoisted. The variable exists before its line of code, but var initializes to undefined.
  • let and const are hoisted too, but they are not accessible before their declaration line, which means accessing them early triggers a ReferenceError. This period is the temporal dead zone.
  • Accidental globals. Always declare with const or let to avoid creating globals on window.
  • Name shadowing. Avoid redeclaring the same name in nested scopes unless intentional.
  • Asynchronous callbacks. Use let in loops or capture values in a new function scope.
  • Create pure functions and small modules to keep scope trees shallow and readable.

Frontend code that wires event handlers, fetches data, or builds media URLs relies on closures to carry context. For example, when building dynamic image URLs for delivery, lexical scope lets you encapsulate parameters while returning a tiny helper.

// Simple Cloudinary-style URL factory using lexical scope
function makeImageUrl({ cloudName, publicId }) {
  const base = `https://res.cloudinary.com/${cloudName}/image/upload/`;
  return (transforms) => `${base}${transforms}/${publicId}`;
}

const toWebP600 = makeImageUrl({ cloudName: "demo", publicId: "sample.jpg" });
const url = toWebP600("c_fill,w_600,f_webp");
// Use with <img src="url"> or your framework's image componentCode language: JavaScript (javascript)

If you are rendering images in markup, see this quick refresher on the HTML image tag. When delivering media at scale, understanding hosting basics helps pick the right architecture and cache strategy, as covered in Understanding image hosting for websites

  • Log values right where you capture them to confirm what the closure sees.
  • Search for var in loops and convert to let where appropriate.
  • Use linters to catch accidental globals and shadowed variables.
  • Lexical scope means variables are available where they are declared and in nested scopes.
  • let and const are block-scoped and avoid loop-closure bugs. var is function-scoped and trickier.
  • Closures let functions remember outer variables. Great for factories, handlers, and configuration.
  • Keep scopes small, avoid accidental globals, and favor clear, scoped helpers for reliability.

Ready to build faster, more reliable frontend code and deliver optimized media at scale? Sign up for a free Cloudinary account and start transforming and delivering assets with confidence.

Start Using Cloudinary

Sign up for our free plan and start creating stunning visual experiences in minutes.

Sign Up for Free