Start with reassignment

JavaScript has three ways to declare a variable: let, const, and var. The choice affects whether you can assign a new value to its name, where that name is available, and what happens if code tries to use it before its declaration. You can run the short examples here one at a time in a modern browser’s developer console. Run each example separately so a declaration from one example does not interfere with another.

Use let when a name needs a new value later: let count = 1; count = 2; console.log(count); prints 2. Use const when the name should keep its initial value: const limit = 3; limit = 4; throws a TypeError. A const declaration also needs an initializer, so const limit; is invalid. var permits reassignment too, but its scope and early-access behavior make it less suitable as a default in new code.

const protects the binding, not everything reachable through it. For example, const settings = { theme: 'light' }; settings.theme = 'dark'; is allowed because the variable still refers to the same object. By contrast, settings = { theme: 'dark' }; attempts to give the variable a different object and fails. If an object’s properties must not change, const alone does not provide that guarantee.

Understand where a declaration is visible

A block is the code between a pair of braces, such as the body of an if statement or a loop. let and const are block-scoped. Run this as one example: if (true) { let message = 'ready'; const status = 'open'; console.log(message, status); } The log inside the block works. Trying console.log(message); or console.log(status); after the closing brace produces a ReferenceError because those names are not available there.

var is function-scoped rather than block-scoped. In a separate example, run: function check() { if (true) { var result = 'ready'; } console.log(result); } check(); The function prints 'ready' even though the declaration appears inside the if block. Outside check, result is not available. A var declared outside a function has different behavior depending on whether code runs as a classic browser script or as a module, so do not assume it is interchangeable with a block-scoped declaration at the top level.

Block scope helps keep a temporary name close to the work it supports. It also avoids accidental sharing between parts of a function. For a new local variable, a useful starting habit is to choose const, switch to let if reassignment is needed, and reserve var mainly for reading or maintaining older code.

What hoisting does—and does not—mean

Hoisting describes how declarations are handled before a scope’s statements execute. It does not mean JavaScript moves your source-code lines. The observable difference is easiest to see by running these examples separately. With var, console.log(score); var score = 10; prints undefined, then the assignment gives score its value. The declaration is available from the start of its function scope, but its initializer runs only when execution reaches it.

let and const declarations are also associated with their scope before execution reaches the declaration. However, they cannot be accessed before initialization. Run console.log(score); let score = 10; on its own and it throws a ReferenceError. Replacing let with const has the same early-access result. The interval before initialization is often called the temporal dead zone. An outer variable with the same name does not bypass it: let value = 'outer'; { console.log(value); let value = 'inner'; } throws because the block’s own value is the one being accessed too early.

The reliable habit is simpler than memorizing edge cases: declare a variable before its first use. This makes the code’s order match the order a reader expects, regardless of which declaration keyword appears.

Loops and names that persist

Scope differences become especially visible when a loop creates callbacks. Run this example on its own: const tasks = []; for (let index = 0; index < 3; index++) { tasks.push(() => index); } console.log(tasks.map(task => task())); It prints [0, 1, 2]. In this loop, let gives each iteration a distinct index binding for callbacks to capture.

Now run the same example separately, changing only let index to var index. It prints [3, 3, 3]. All three callbacks refer to the same function-scoped index, and they run after the loop has finished. This is a scope issue, not a delay or timing requirement: the callbacks in both examples are invoked only after their loops end.

These examples use a basic counting loop, where the counter is reassigned. That makes let a natural fit. For a name created once within an iteration, const may be clearer. Choose the declaration according to what the name must do, and keep its scope as narrow as practical.

Back to category ↗