JavaScript let: Block Scope, TDZ, and Practical Patterns

I still remember the first time a production bug came from a loop variable that “mysteriously” changed value. The UI showed the wrong customer name in a confirmation modal, and the only clue was a setTimeout callback that logged the same number for every row. That bug wasn’t flashy, but it was expensive because it broke trust. Since then, I’ve been picky about variable declarations. In modern JavaScript, let is the tool I reach for when I need a value that can change but should stay inside a specific block. It’s simple on the surface, yet the details—block scope, the temporal dead zone, and safer closure behavior—shape how your code behaves in real systems.

I’m going to walk through how let actually works, where it saves you from bugs, and where it can still surprise you. You’ll see runnable examples, learn when I pick let vs const, and get a set of rules I use in 2026 codebases that mix browsers, Node.js, and TypeScript. If you’ve ever wondered why “just switch var to let” sometimes fixes a bug, this will make that answer concrete.

The Core Idea: Block Scope That Matches Your Mental Model

When I explain let to new engineers, I use a small physical analogy: imagine each {} block as a room with a whiteboard. A let variable is written on the whiteboard inside that room. When you walk out, you can’t see the whiteboard anymore. That’s block scope, and it matches how most of us naturally think about code.

Here is the basic syntax, nothing fancy:

let total = 0;

The key is where that line lives. If it lives inside a block, the name is visible only there. This is a big shift from var, which is scoped to the whole function. In day-to-day work, this means let keeps local details local.

function calculateInvoice(items) {

let subtotal = 0; // visible throughout the function

if (items.length === 0) {

let reason = "No items"; // visible only inside the if block

return { subtotal, reason };

}

// reason is not visible here

for (let item of items) {

subtotal += item.price;

}

return { subtotal };

}

This behavior has a practical payoff: you can reuse names in different blocks without leaking values across branches. That reduces accidental coupling and makes refactors safer. It also encourages you to think about the lifespan of data, which is a big deal once your functions exceed a few lines.

Hoisting Still Exists, But the Temporal Dead Zone Changes Everything

A lot of people say “let isn’t hoisted,” but that’s not quite accurate. let is hoisted, yet it’s uninitialized until the declaration is evaluated. That gap is called the temporal dead zone (TDZ). In plain terms: the name exists, but you are not allowed to touch it yet.

console.log(orderId); // ReferenceError: Cannot access ‘orderId‘ before initialization

let orderId = "INV-2049";

Here’s why this matters in real code. With var, the variable exists and is initialized to undefined, which can mask bugs. The TDZ forces you to declare variables before use, which is a healthy constraint for large codebases.

Consider this pattern that often appears in UI logic:

if (isPriority) {

console.log(note); // TDZ if note is declared below

let note = "Priority handling";

sendAlert(note);

}

This throws, and that’s good. It makes the problem visible early instead of letting the code run with undefined. In my experience, this catches bugs during development rather than at runtime for real users.

In 2026 tooling, ESLint and TypeScript both flag this kind of mistake quickly, but the language itself is already doing the right thing. The TDZ is like a safety barrier around a newly installed piece of machinery: you can see it, but you can’t touch it until it’s ready.

Loops and Closures: The Place Where let Pays for Itself

The biggest everyday win for let is in loops, especially when asynchronous code is involved. Each iteration gets its own binding, which means closures capture the value you expect.

const handlers = [];

for (let i = 0; i < 3; i++) {

handlers.push(() => console.log("Clicked card", i));

}

handlers[0]();

handlers[1]();

handlers[2]();

Output:

Clicked card 0

Clicked card 1

Clicked card 2

If you used var here, all three handlers would print the same number, because var has function scope. You can fix that with an IIFE, but that adds noise. let removes that need.

Here’s a more realistic example with a timer and a list of user IDs:

const userIds = [101, 205, 309];

for (let index = 0; index < userIds.length; index++) {

const id = userIds[index];

setTimeout(() => {

// index and id are fixed for each iteration

console.log(Sync user ${id} at row ${index});

}, 200);

}

In modern codebases, I see this pattern in React event handlers, Node.js cron tasks, and data processing scripts. let keeps closures honest, and that’s often the difference between a one-line fix and a long debugging session.

Redeclaration, Shadowing, and the Names You Can Trust

Another place where let earns its keep is by refusing to let you redeclare a variable in the same scope. This is a guardrail, and in teams it prevents accidental overwrites.

let sessionToken = "abc123";

let sessionToken = "def456"; // SyntaxError

You can, however, shadow a variable in a nested block. That’s allowed, and sometimes it’s exactly what you want. I treat this as a power tool: useful, but only when I’m explicit about intent.

let status = "draft";

if (status === "draft") {

let status = "review"; // shadowed within the block

console.log("Inner status:", status);

}

console.log("Outer status:", status);

Output:

Inner status: review

Outer status: draft

Shadowing can be clean if it mirrors the problem domain, like a temporary override inside a short block. But if the block is long, I rename the inner variable to avoid confusion. That’s a habit I recommend if you care about readability.

let vs const vs var: A Clear Rule Set

When I compare approaches, I use a table so the trade-offs are concrete. Here’s a practical view I use with teams:

Feature

var

let

const

Scope

Function

Block

Block

Hoisting Behavior

Hoisted + initialized undefined

Hoisted + TDZ

Hoisted + TDZ

Redeclaration

Allowed in same scope

Not allowed

Not allowed

Reassignment

Allowed

Allowed

Not allowed

Common Use in 2026

Legacy only

Mutable block values

Default choiceMy rule set is straightforward:

  • I default to const for everything that doesn’t change.
  • I use let when a value must change inside a block or loop.
  • I avoid var unless I’m in a legacy file and rewriting it would cause risk.

This is not about style points. This is about reducing the number of states a variable can take. When you limit reassignments, you reduce mental load. And when you need reassignments, let makes that intent explicit.

Real-World Patterns Where let Shines

Here are patterns I use in production that get safer with let:

1) Accumulators in a Block

function summarizeCart(items) {

let total = 0;

let maxPrice = 0;

for (let item of items) {

total += item.price;

if (item.price > maxPrice) {

maxPrice = item.price;

}

}

return { total, maxPrice };

}

This is the classic case: values change as you iterate, and you want those changes visible in the loop and after it. let is clear and honest here.

2) Conditional Preparation

function buildPayload(user, plan) {

let payload = { userId: user.id, planId: plan.id };

if (plan.isTrial) {

payload = { ...payload, trialEndsAt: plan.trialEndsAt };

}

return payload;

}

I could use const with a new name inside the block, but let keeps it tidy. The key is that the variable’s intent is “final payload,” and reassigning it aligns with that intent.

3) Two-Phase Initialization

function parseConfig(raw) {

let config;

if (raw.startsWith("{")) {

config = JSON.parse(raw);

} else {

config = { path: raw };

}

return config;

}

This is a common parser pattern. let lets me pick the final shape based on input without widening scope to the entire function via var.

When Not to Use let

Here’s where I say “no” to let:

1) Values that should never change. If you’re building a data object or pulling a dependency, use const. If a value is reassignable by default, someone will change it later by accident.

2) API surface constants. Configuration like feature flags or route names should be const. It makes the API stable and easier to reason about.

3) Values reused across unrelated blocks. If I see let declared at the top of a long function and assigned in many branches, I stop and split the function. That pattern is usually a sign the function is doing too much.

Here’s a tiny example of the third case:

function processEvent(event) {

let action;

if (event.type === "signup") action = "welcome";

if (event.type === "payment") action = "receipt";

if (event.type === "refund") action = "refund";

sendEmail(action, event.userId);

}

This works, but the intent is fuzzy. I’d rather map event types to actions and keep each branch pure. let is not the problem here; it’s a signal that the shape of the function can improve.

Common Mistakes I Still See (and How I Avoid Them)

Mistake 1: Declaring let After Use

if (isActive) {

toggle(featureName); // TDZ crash

let featureName = "search";

}

Fix: declare first, use second. I treat declarations as the “top of the room,” just like I used to treat var, but now it’s required.

Mistake 2: Shadowing Across a Long Block

let theme = "light";

if (user.isAdmin) {

let theme = "dark"; // easy to miss in long blocks

applyTheme(theme);

}

Fix: if the block is long, use a new name like adminTheme.

Mistake 3: Using let When const Would Do

let apiHost = "https://api.example.com"; // should be const

Fix: default to const and let the compiler or linter tell you when reassignment is needed. This keeps your mental model tight.

Mistake 4: Mixing var and let in the Same Function

var count = 0;

for (let i = 0; i < 5; i++) {

count += i;

}

Fix: I keep var out of new code. Mixing the two makes it harder to track scope rules and can confuse new contributors.

Performance Notes You Actually Need

Most of the time, choosing let over var has no meaningful performance impact. Modern JavaScript engines are built around block scoping and can handle let efficiently. In my own micro-benchmarks with 100,000 to 1,000,000 loop iterations, I usually see differences in the 0–5ms range on a modern laptop, which is noise for real apps. The performance impact you should care about is in your logic, not in whether you picked let or var.

There is one practical performance angle: correctness saves time. A bug fixed early is much cheaper than a bug found after release. let helps correctness by forcing tighter scope and blocking accidental early access. That is the performance win that matters in teams.

let in Modern Tooling and TypeScript Workflows

Even if you’re writing plain JavaScript, you probably run ESLint or a similar linter. In 2026, most standard configs encourage const by default and allow let when reassignment happens. That pairs nicely with let because it makes intent visible. I often pair this with TypeScript or JSDoc types so the variable name, mutability, and data shape all align.

Here’s a pattern I use in shared packages:

/ @type {number} */

let retryCount = 0;

function recordFailure() {

retryCount += 1;

}

Even without TypeScript, the JSDoc makes intent clear. In TS code, the same idea works with let retryCount: number = 0;.

In AI-assisted workflows, I’ve found that explicit let usage helps code-generation tools avoid refactor mistakes. If you mark something as const, the model is more likely to keep it stable. If you need mutation, let tells the model it can update the value. It’s a tiny signal, but it helps when you’re iterating quickly.

A Quick Mental Checklist I Use Before Writing let

I run this in my head in a few seconds:

1) Does this value need to change? If no, I choose const.

2) Does it only exist in this block? If yes, let is perfect.

3) Will this be used across many branches? If yes, I consider restructuring the function.

4) Will a closure capture it? If yes, I verify the loop behavior and confirm that let is creating a new binding per iteration.

That checklist keeps my code predictable. It also makes my future self happier because the intent is clear from the declaration line.

A 5th-Grade Analogy That Actually Works

Think of let like writing a note on a sticky note and putting it on a door inside your house. The note is visible only in that room. If you leave the room, the note stays behind. var is like writing the same note on every door in the house, whether you wanted it there or not. const is like laminating the note so you can’t erase it. This is why I trust let for values that should change inside one room only.

A Deeper Look at Block Boundaries (If/Else, Switch, Try/Catch)

Block scope sounds straightforward until you run into blocks that are visually confusing. I’ve seen let bugs crop up in switch statements and try/catch sections because the blocks are easy to read incorrectly.

If/Else Blocks

Each if, else if, and else block is its own scope. That means you can have the same name in each block without collision. This can be a blessing if you’re isolating logic and a curse if you expect a name to “carry over.”

if (user.isActive) {

let label = "Active";

console.log(label);

} else {

let label = "Inactive";

console.log(label);

}

// label is not accessible here

Switch Statements and the Missing Braces Problem

A switch statement is a little tricky because all case clauses live in the same block unless you add braces. That means let declarations can collide across cases.

switch (status) {

case "new":

let message = "Welcome";

console.log(message);

break;

case "returning":

let message = "Welcome back"; // SyntaxError in the same block

console.log(message);

break;

}

Fix it by adding braces per case to create a block scope:

switch (status) {

case "new": {

let message = "Welcome";

console.log(message);

break;

}

case "returning": {

let message = "Welcome back";

console.log(message);

break;

}

}

Try/Catch Scoping

catch introduces its own block, and the error variable is scoped to that block only.

try {

risky();

} catch (err) {

let message = Failed: ${err.message};

report(message);

}

// err and message are not accessible here

This helps keep error details from leaking into other paths, which is exactly what you want in robust error handling.

The Temporal Dead Zone in Real-Life Scenarios

The TDZ is more than a theoretical concept. It bites in these practical situations:

1) Default Parameters That Reference let

Default parameters are evaluated before the function body runs, so they can’t use variables declared inside the body.

function createBanner(text = title) {

let title = "Welcome";

return ${title}: ${text};

}

// ReferenceError because title is in the TDZ during parameter evaluation

Fix: move the default inside the function or reorder the logic.

function createBanner(text) {

let title = "Welcome";

if (text === undefined) text = title;

return ${title}: ${text};

}

2) Self-Referential Initialization

Sometimes people try to build a variable based on its previous value during initialization. That doesn’t work with let.

let counter = counter || 0; // ReferenceError (counter is in the TDZ)

Fix: set a default value directly or use a separate variable name.

3) TDZ Across Imports and Modules

In module scope, let is still block-scoped but the TDZ can surface in subtle import timing issues. If you rely on top-level let variables that are used before initialization, you will get runtime errors on module load. That’s especially painful in build pipelines where errors show up only after bundling.

The best fix is simple: declare before use and keep module-level let minimal.

Understanding Global Scope: let vs var in the Browser

This is one of the most practical differences if you work in browsers. A var declaration at top level becomes a property on the window object; let does not.

var globalVar = "visible";

let globalLet = "hidden";

console.log(window.globalVar); // "visible"

console.log(window.globalLet); // undefined

This matters because window pollution creates accidental coupling between scripts. If you have multiple bundles or third-party scripts, var globals can overwrite each other. let avoids that. It still creates a global binding, but it doesn’t attach to window.

In Node.js modules, top-level let is scoped to the module, not to the entire process. That gives you a clean boundary by default, which is one reason modern Node code avoids var entirely.

let and Destructuring: A Cleaner Way to Reassign

Destructuring works well with let, especially when you want to update values in a controlled way. I use this with arrays, object results, and function returns.

Array Destructuring with Reassignment

let min = 10;

let max = 5;

if (min > max) {

[min, max] = [max, min]; // swap values

}

This is a tidy pattern, and it’s a place where let makes the intent explicit: these values can change, but only here.

Object Destructuring with Rebinding

let current = { id: 1, name: "Alex" };

function updateName(name) {

current = { ...current, name };

}

I use let here because the variable current points to a new object each time. That’s a clear and safe use of reassignment.

let with Async/Await and Concurrency

Async code is where scoping mistakes become the most expensive, because the bugs show up later and often in production. let helps keep values stable in the right places, but you still need to be deliberate.

Sequential Async Loop

async function syncUsers(users) {

for (let user of users) {

await syncUser(user);

console.log("Synced", user.id);

}

}

The let binding for user is new each iteration, which is exactly what you want. The loop is sequential; it waits for each sync to finish.

Parallel Async with Captured Index

async function syncUsersParallel(users) {

const tasks = [];

for (let i = 0; i < users.length; i++) {

tasks.push(

syncUser(users[i]).then(() => {

console.log(Synced ${users[i].id} at index ${i});

})

);

}

await Promise.all(tasks);

}

Because i is a let, each closure gets the right value. If you used var, every log would show the final index, and you’d spend an hour debugging a race that isn’t actually a race.

let as a Signal: Communicating Intent to Humans

One of the best things about let is that it’s not just for the runtime; it’s for the reader. A line like this tells me three things instantly:

let retryDelay = 100;

1) This value will change.

2) It is local to the block.

3) The developer intends to update it.

That’s a lot of information from a single keyword. When I read const, I know I can trust the value; when I read let, I know to look for where it changes. This is why I insist on prefer-const in linting—it keeps let meaningful.

Edge Cases That Surprise Even Experienced Devs

These are the kinds of “wait, what?” moments I still see in code reviews.

1) let in the Same Scope as a Function Declaration

You cannot redeclare a name with let if it was already used for a function declaration in the same scope.

function report() {}

let report = 42; // SyntaxError

This is different from var, which would allow it. The fix is to rename one of them or move the let into a new block.

2) let in a Block That Looks Like a Statement

This is about readability rather than correctness, but it matters. If you write a one-line if without braces, you can’t declare let in it.

if (isReady) let msg = "Go"; // SyntaxError

You need braces:

if (isReady) {

let msg = "Go";

run(msg);

}

3) for...in and for...of Scoping

Both loops create a new binding per iteration with let, which is great, but the binding is to the iteration variable, not the object or array itself. If you store references, make sure you understand what you’re capturing.

const users = [{ id: 1 }, { id: 2 }];

const handlers = [];

for (let user of users) {

handlers.push(() => console.log(user.id));

}

handlers[0](); // 1

handlers[1](); // 2

This behaves how you expect. If you mutate user inside the loop, the closures capture the updated version per iteration, not a single shared one.

Alternative Approaches and Why let Still Wins

Before let, we used patterns like IIFEs and helper functions to create scope. They still work, and in some cases they’re still useful, but they’re more verbose than they need to be.

The Old IIFE Fix

var handlers = [];

for (var i = 0; i < 3; i++) {

(function (index) {

handlers.push(function () {

console.log(index);

});

})(i);

}

This works, but it’s heavy. It also hides the main idea in plumbing. let expresses intent directly:

const handlers = [];

for (let i = 0; i < 3; i++) {

handlers.push(() => console.log(i));

}

The “New Variable” Style with const

Sometimes I prefer const by creating new variables inside blocks rather than reassigning.

const base = { id: user.id };

const payload = user.isTrial

? { ...base, trialEndsAt: user.trialEndsAt }

: base;

That’s clean, and it’s a good alternative if reassignment feels messy. I still use let when the flow is more naturally sequential, but it’s worth knowing this style exists.

Using reduce Instead of let

Functional patterns can avoid mutation entirely. For example:

const total = items.reduce((sum, item) => sum + item.price, 0);

I use reduce when it reads well, but it can become less clear with complex logic. I won’t force a functional style just to avoid let. Readability wins.

Practical Scenarios: Choosing the Right Tool

Here are some practical “what would I do?” moments that come up in real projects.

Scenario 1: Building a Dynamic SQL Query

function buildQuery(filters) {

let query = "SELECT * FROM orders WHERE 1=1";

if (filters.status) {

query += AND status = ‘${filters.status}‘;

}

if (filters.minTotal) {

query += AND total >= ${filters.minTotal};

}

return query;

}

This is a classic case where let is natural because the query is assembled step by step. In production, I’d also use parameterized queries to avoid injection, but the let pattern is still valid.

Scenario 2: Pagination and Cursor State

async function fetchAllPages(fetchPage) {

let cursor = null;

let all = [];

while (true) {

const { items, nextCursor } = await fetchPage(cursor);

all = all.concat(items);

if (!nextCursor) break;

cursor = nextCursor;

}

return all;

}

cursor and all change with each loop. If you try to force const here, you’ll either create unnecessary intermediate variables or hurt clarity.

Scenario 3: Form Validation with Mutable Errors

function validate(form) {

let errors = {};

if (!form.email) errors = { ...errors, email: "Required" };

if (!form.age || form.age < 18) errors = { ...errors, age: "Must be 18+" };

return errors;

}

You can also use a const and mutate the object, but I often prefer reassigning a new object to keep the logic immutable while still using let for rebinds.

Debugging Advice: Use let to Narrow the Blast Radius

When I debug complex functions, I often introduce let variables inside smaller blocks to isolate values. It makes logs and breakpoints more reliable because I can be confident about scope boundaries.

A practical trick: if a variable needs to be logged or inspected in several branches, I’ll create a new block just for that logic. That keeps the value near where it’s used and prevents accidental reuse.

function resolvePricing(plan, user) {

if (plan.isLegacy) {

{

let price = plan.legacyPrice;

logPricing(user.id, price, "legacy");

return price;

}

}

{

let price = plan.currentPrice;

logPricing(user.id, price, "current");

return price;

}

}

This might look verbose, but it makes the intent crystal clear, and it prevents future edits from accidentally mixing the two paths.

let and Testing: How Scope Choices Affect Testability

Testing isn’t directly about let, but scope choices shape the code you test. Overuse of let at top level can create functions with too many moving parts. When I keep let scoped to smaller blocks, I end up with functions that have fewer branches and are easier to test.

If a function needs a single let that changes a dozen times, I usually split the logic into helper functions. That means each helper can use const internally and the higher-level orchestration can use let sparingly. The result is cleaner tests and fewer mocks.

Modern Lint Rules That Reinforce Good let Usage

These are the rules I see in mature codebases, and they make let usage more intentional:

  • prefer-const: If a variable isn’t reassigned, the linter asks for const.
  • no-use-before-define: Prevents TDZ mistakes early.
  • no-shadow: Warns when shadowing hides a name from an outer scope.
  • block-scoped-var: Catches legacy var scoping issues.

When these rules are active, let becomes the exception rather than the default, which is exactly how I like it.

Performance Considerations: Where It Can Matter (and Mostly Doesn’t)

Earlier I said let vs var is not a real performance issue. That’s still true, but there are two subtle aspects worth understanding:

1) Optimization stability. Engines can optimize code more reliably when scopes are clear and variables don’t leak. let helps by preventing unintended shared bindings.

2) Garbage collection pressure. With smaller scopes, values become unreachable sooner. In long-running processes, that can reduce memory pressure. This isn’t a reason to pick let, but it’s a nice side effect of good scoping.

If performance is a problem, it’s almost never because of let. It’s because of algorithmic complexity, excessive allocations, or heavy I/O.

Migration Notes: Converting var to let Safely

If you’re modernizing a codebase, “replace all var with let” is tempting, but it can create subtle bugs if you don’t look carefully. Here’s how I do it safely:

1) Start with const. Replace var with const first where possible. Let the compiler or linter tell you which ones need let.

2) Check loops with closures. The loop variable change can alter behavior. In most cases it fixes bugs, but verify.

3) Watch for hoisting differences. If code relied on undefined from var, let will throw. Fix the order instead of fighting the TDZ.

4) Handle switch cases. Add braces to avoid redeclaration errors.

This approach avoids surprises and turns a risky refactor into a steady improvement.

A More Complete Example: Payment Retry Logic

Here’s a real-world style example that combines let, const, async handling, and clear scoping.

async function processPayment(order, chargeCard) {

const maxRetries = 3;

let attempt = 0;

let lastError = null;

while (attempt < maxRetries) {

attempt += 1;

try {

const receipt = await chargeCard(order);

return { ok: true, receipt, attempts: attempt };

} catch (err) {

lastError = err;

if (attempt >= maxRetries) break;

await wait(200 * attempt); // backoff

}

}

return { ok: false, error: lastError, attempts: attempt };

}

Here attempt and lastError are mutable state across iterations, which is a clear and honest use of let. Everything else stays const.

Another Example: Filtering and Enrichment Pipeline

This is the kind of data processing logic where let keeps the flow readable.

function enrichUsers(users, lookupPlan) {

let results = [];

for (let user of users) {

if (!user.active) continue;

let plan = lookupPlan(user.planId);

if (!plan) {

plan = { name: "Unknown", tier: "free" };

}

results = results.concat({

id: user.id,

name: user.name,

planName: plan.name,

planTier: plan.tier,

});

}

return results;

}

You could rewrite this in a more functional style, but the let usage is very readable and matches the procedural flow.

Subtle Scoping in Nested Blocks

Sometimes I intentionally create a nested block to narrow scope. This is not common, but it can be a powerful clarity tool in complex logic.

function calculateDiscount(order) {

let discount = 0;

if (order.total > 100) {

{

let rate = 0.05;

discount += order.total * rate;

}

}

if (order.coupon) {

{

let rate = order.coupon.rate;

discount += order.total * rate;

}

}

return discount;

}

Each inner block keeps rate isolated. This prevents accidental reuse and keeps each section self-contained. I don’t use this everywhere, but it’s a useful technique when refactoring large functions.

How I Teach let to New Developers

When I onboard new engineers, I keep it simple:

1) Start with const everywhere.

2) Switch to let only when you need reassignment.

3) Keep let as close as possible to where it’s used.

4) If you need let across many branches, consider refactoring.

Then I show them the loop closure example and the switch-case block issue. Those two examples cover 80% of misunderstandings.

A Small Table of “Smell Checks”

These aren’t hard rules, but they keep me honest:

Smell

Why It’s Suspicious

What I Do —

let at top of a long function

Function likely doing too much

Split or extract helper let used only once

Might be const

Prefer const let shadowing in deep blocks

Hard to follow

Rename inner variable let + var together

Confusing rules

Normalize to let/const

Closing: What I Want You to Do Next

If you take one thing from this, make it this: let is about clarity. When you declare with let, you’re telling yourself and your team, “this value can change, but only here.” That message prevents scope leakage and avoids the kinds of bugs that are painful to debug. I still see codebases where var slips in, and I can almost predict the class of bugs that will follow. You can avoid that entirely by being deliberate with let and const from the start.

Your next step is simple and practical. Pick a small file in your codebase and scan for var. Replace it with let or const and run your tests. Then, look at any long functions that declare let at the top and mutate it in many branches. If the intent feels muddy, split the function and keep the variables closer to where they are used. That one change pays dividends in readability and fewer late-night fixes.

If you’re teaching this to a teammate, lead with the loop example that captures the right value in a callback. It’s tangible and shows the real-world impact immediately. In my experience, once people see that, the rest of the rules fall into place. You end up with code that reads like a story: each block introduces the values it needs, changes them when necessary, and then lets them go. That’s the level of clarity I aim for in 2026 JavaScript.

Expansion Strategy

Add new sections or deepen existing ones with:

  • Deeper code examples: More complete, real-world implementations
  • Edge cases: What breaks and how to handle it
  • Practical scenarios: When to use vs when NOT to use
  • Performance considerations: Before/after comparisons (use ranges, not exact numbers)
  • Common pitfalls: Mistakes developers make and how to avoid them
  • Alternative approaches: Different ways to solve the same problem

If Relevant to Topic

  • Modern tooling and AI-assisted workflows (for infrastructure/framework topics)
  • Comparison tables for Traditional vs Modern approaches
  • Production considerations: deployment, monitoring, scaling
Scroll to Top