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:

StepDecisionWhy
1Start with constMost variables in test code should not be reassigned
2Change to let only if the value must changeCounters, retry attempts, and values built step by step need reassignment
3Avoid var in modern QA codeIt has function scope, allows redeclaration, and can hide bugs
javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
const token = undefined;

console.log(token); // undefined

This is valid because the variable has an initial value. But this is not valid:

javascriptjavascript
// SyntaxError: Missing initializer in const declaration
// const token;

If you plan to assign a value later, use let.

javascriptjavascript
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.

javascriptjavascript
const user = { name: "Hayk" };

user.name = "Alik";

console.log(user); // { name: "Alik" }

You cannot make user point to a new object:

javascriptjavascript
// TypeError: Assignment to constant variable.
// user = { name: "Ani" };

In QA fixtures, prefer creating a new object instead of mutating shared data:

javascriptjavascript
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.

javascriptjavascript
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:

javascriptjavascript
// TypeError: Assignment to constant variable.
// ids = [10, 20];

When possible, build a new array to avoid changing shared test data:

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
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.

javascriptjavascript
console.log(score); // undefined

var score = 5;

JavaScript behaves roughly like this:

javascriptjavascript
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.

javascriptjavascript
// 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:

javascriptjavascript
// 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.

javascriptjavascript
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

javascriptjavascript
const expectedHeading = "Dashboard";
const expectedStatusCode = 200;

Use let for counters or values built step by step

javascriptjavascript
let failedChecks = 0;

if (actualTitle !== "Dashboard") {
  failedChecks += 1;
}

console.log(failedChecks);

Avoid var in loops

javascriptjavascript
const testNames = ["login", "checkout", "profile"];

for (let index = 0; index < testNames.length; index += 1) {
  console.log(testNames[index]);
}

15. Decision table

QuestionUse
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

TopicTakeaway for QA automation
constStable binding, best default
letReassign only when the value intentionally changes
varFunction-scoped, redeclarable, avoid in modern code
Object mutationconst does not freeze object properties
Array mutationconst does not freeze array items
Hoistingvar gives undefined; let and const fail before initialization
Block scopelet and const stay inside {}

Suggested exercises

  1. Create examples where const, let, and var behave differently.
  2. Mutate a const object property and explain why it works.
  3. Mutate a const array item and explain why it works.
  4. Compare var and let inside a for loop.
  5. 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:

javascriptjavascript
console.log(value);
var value = 5;
Task 9: hoisting with let

Run this code and explain the error:

javascriptjavascript
console.log(value);
let value = 5;