Lesson 12 of 14
Lesson 12 — var, let, const, and hoisting
Title: Variable Declarations, Reassignment, Mutation, Scope, and Hoisting
Description: This lesson compares var, let, and const in JavaScript. var is function-scoped, can be redeclared, and is hoisted with an initial value of undefined. let and const are block-scoped and cannot be used before declaration due to the temporal dead zone. It also explains the difference between reassignment (changing a variable) and mutation (changing an object), as well as how hoisting affects variable behavior.
Why it matters for QA: Test data often includes mutable values like users, payloads, counters, or expected results. Using the wrong type of declaration (especially var) can cause hidden bugs, such as shared state between loops or incorrect initialization order. Proper use of let and const helps keep test behavior
1. The short rule
Use this three-step decision:
| Step | Decision | Why |
|---|---|---|
| 1 | Start with const | Most variables in test code should not be reassigned |
| 2 | Change to let only if the value must change | Counters, retry attempts, and values built step by step need reassignment |
| 3 | Avoid var in modern QA code | It has function scope, allows redeclaration, and can hide bugs |
const baseUrl = "https://example.com";
let retryCount = 0;
retryCount += 1;
console.log(baseUrl);
console.log(retryCount);
Operational reading:
const: stable binding (baseUrl).let: intentional reassignment (retryCount).var: unnecessary—offers no advantage under modern module bundling.
2. const means no reassignment
You cannot assign a new value to a const variable.
const testStatus = "passed";
// TypeError: Assignment to constant variable.
// testStatus = "failed";
console.log(testStatus);
Use const when the variable name should always point to the same value.
3. let allows reassignment
Use let when the value must change.
let remainingRetries = 2;
remainingRetries -= 1;
remainingRetries -= 1;
console.log(remainingRetries); // 0
If a variable never changes, use const. It tells the reader that the value is stable.
4. const with undefined
const must be initialized immediately.
const token = undefined;
console.log(token); // undefined
This is valid because the variable has an initial value. But this is not valid:
// SyntaxError: Missing initializer in const declaration
// const token;
If you plan to assign a value later, use let.
let sessionToken;
sessionToken = "abc-123";
console.log(sessionToken);
5. const objects can still be mutated
const protects the variable binding, not the inside of an object.
const user = { name: "Hayk" };
user.name = "Alik";
console.log(user); // { name: "Alik" }
You cannot make user point to a new object:
// TypeError: Assignment to constant variable.
// user = { name: "Ani" };
In QA fixtures, prefer creating a new object instead of mutating shared data:
const defaultUser = { name: "Hayk", role: "viewer" };
const adminUser = { ...defaultUser, role: "admin" };
console.log(adminUser);
6. const arrays can still be mutated
The same rule applies to arrays.
const ids = [1, 2, 3];
ids[0] = 9;
ids.push(4);
console.log(ids); // [9, 2, 3, 4]
You cannot reassign the variable to a new array:
// TypeError: Assignment to constant variable.
// ids = [10, 20];
When possible, build a new array to avoid changing shared test data:
const originalIds = [1, 2, 3];
const updatedIds = [9, ...originalIds.slice(1)];
console.log(updatedIds);
7. Redeclaration rules
let and const do not allow redeclaring the same name in the same scope.
let status = "created";
// SyntaxError: Identifier 'status' has already been declared
// let status = "updated";
status = "updated";
console.log(status);
var allows redeclaration, which can hide mistakes.
var browserName = "chromium";
var browserName = "firefox";
console.log(browserName); // firefox
This is one reason modern code avoids var.
8. Block scope: let and const
let and const are scoped to the nearest block.
if (true) {
let message = "inside block";
const statusCode = 200;
console.log(message);
console.log(statusCode);
}
// console.log(message); // ReferenceError
// console.log(statusCode); // ReferenceError
This is the behavior you usually want in tests.
9. Function scope: var
var ignores block scope and is scoped to the nearest function.
function printIndex() {
for (var index = 0; index < 3; index += 1) {
console.log(index);
}
console.log(index); // 3
}
printIndex();
The loop finished, but index is still visible inside the function. With let, it would be hidden after the loop.
10. var outside a function
In old browser scripts, top-level var can become a property on window.
var oldStyleValue = 5;
let modernValue = 7;
console.log(window.oldStyleValue); // 5 in classic browser scripts
console.log(window.modernValue); // undefined
11. Hoisting with var
var declarations are hoisted and initialized with undefined.
console.log(score); // undefined
var score = 5;
JavaScript behaves roughly like this:
var score;
console.log(score);
score = 5;
This can hide bugs because the code runs instead of failing early.
12. Hoisting with let and const
let and const are hoisted too, but they are not initialized until their declaration line runs.
// ReferenceError: Cannot access 'userName' before initialization
// console.log(userName);
let userName = "qa";
console.log(userName);
The time before the declaration line is called the temporal dead zone.
const behaves the same, and it also requires an initial value:
// ReferenceError: Cannot access 'token' before initialization
// console.log(token);
const token = "abc-123";
console.log(token);
13. Function scope with var
var is not visible outside the function where it was declared.
function prepareRun() {
var runId = "run-001";
console.log(runId);
}
prepareRun();
// console.log(runId); // ReferenceError
So var is function-scoped, not truly global everywhere.
14. Practical examples from QA code
Use const for expected values
const expectedHeading = "Dashboard";
const expectedStatusCode = 200;
Use let for counters or values built step by step
let failedChecks = 0;
if (actualTitle !== "Dashboard") {
failedChecks += 1;
}
console.log(failedChecks);
Avoid var in loops
const testNames = ["login", "checkout", "profile"];
for (let index = 0; index < testNames.length; index += 1) {
console.log(testNames[index]);
}
15. Decision table
| Question | Use |
|---|---|
| Will this variable be reassigned? | let |
| Should this binding stay stable? | const |
| Do I need old function-scoped behavior? | Usually no |
| Am I writing modern QA automation code? | Avoid var |
| Am I changing an object property? | const still allows mutation |
Official docs
Quick recap
| Topic | Takeaway for QA automation |
|---|---|
const | Stable binding, best default |
let | Reassign only when the value intentionally changes |
var | Function-scoped, redeclarable, avoid in modern code |
| Object mutation | const does not freeze object properties |
| Array mutation | const does not freeze array items |
| Hoisting | var gives undefined; let and const fail before initialization |
| Block scope | let and const stay inside {} |
Suggested exercises
- Create examples where
const,let, andvarbehave differently. - Mutate a
constobject property and explain why it works. - Mutate a
constarray item and explain why it works. - Compare
varandletinside aforloop. - Predict the output of hoisting examples before running them.
Homework
Short tasks (about 25-35 minutes). Click a task title to reveal the prompt.
Task 1: choose const or let
Create expectedStatusCode = 200 and retryCount = 0. Pick the correct declaration for each variable, then increment retryCount.
Task 2: const and undefined
Run const token = undefined; console.log(token);. Then try const sessionToken; and explain why it fails.
Task 3: const object mutation
Create const user = { name: "Hayk" }. Change user.name to "Alik". Then try to assign a new object to user and explain the difference.
Task 4: const array mutation
Create const ids = [1, 2, 3]. Change the first item to 9, then push 4. Explain why both operations are allowed.
Task 5: redeclaration
Try to declare let status twice in the same scope. Then try the same with var status. Explain why var can hide bugs.
Task 6: block scope with let
Create for (let index = 0; index < 5; index += 1) {}. Try to log index after the loop and explain the result.
Task 7: function scope with var
Create a function with a for (var index = 0; index < 5; index += 1) {} loop. Log index after the loop but still inside the function. Explain why it prints 5.
Task 8: hoisting with var
Run this code and explain the output:
console.log(value);
var value = 5;
Task 9: hoisting with let
Run this code and explain the error:
console.log(value);
let value = 5;