Back to thoughts

Deep Dive: JavaScript Core Concepts & Internals

22 m read

I put this together while going back through JavaScript fundamentals properly — mostly triggered by the Namaste JavaScript series, backed by my own hands-on practice. It's less "intro to JS" and more the stuff that actually gets asked in interviews and actually explains the weird behavior you hit in production: why var inside a loop breaks your setTimeout, why closures leak memory if you're not careful, why this changes depending on how you call a function rather than where you defined it.

I'm keeping this as one long reference rather than splitting it into a series — that way it's one page to search and revisit.

1. How JavaScript Works — Execution Context

JavaScript is a synchronous, single-threaded language. Everything happens inside an Execution Context.

What is an Execution Context?

An Execution Context is like a big box with two components:

ComponentAlso Known AsContains
Memory ComponentVariable EnvironmentVariables & functions stored as key-value pairs
Code ComponentThread of ExecutionCode executed one line at a time, in order

Two Phases of Execution

When JavaScript runs a program, it creates a Global Execution Context (GEC) in two phases:

Phase 1 — Memory Creation (Creation Phase):

  • JS scans the entire code
  • Allocates memory to all variables and functions
  • Variables are assigned undefined
  • Functions are stored as their entire code

Phase 2 — Code Execution:

  • JS runs through the code line by line
  • Variables get their actual values
  • When a function is invoked, a brand new Execution Context is created inside the GEC

The Call Stack

The Call Stack manages the order of execution of execution contexts.

| function b() EC  |  ← Top (currently executing)
| function a() EC  |
| Global EC (GEC)  |  ← Bottom (always present)
|___________________|
  • When a function is invoked → its EC is pushed onto the stack
  • When a function finishes → its EC is popped off the stack
  • When the program finishes → the GEC is popped, and the stack is empty

Other names for Call Stack: Execution Context Stack, Program Stack, Control Stack, Runtime Stack, Machine Stack

2. Hoisting

Hoisting is JavaScript's behavior of moving declarations to the top of their scope during the memory creation phase.

// ✅ This works — function is fully hoisted
getName();          // "Namaste JavaScript"
console.log(x);    // undefined (var is hoisted with value undefined)

var x = 7;

function getName() {
    console.log("Namaste JavaScript");
}

How Hoisting Works Behind the Scenes

Declaration TypeHoisted?Initial Value
var✅ Yesundefined
function declaration✅ YesEntire function code
let / const✅ Yes (but in TDZ)❌ Cannot access before declaration
Function Expression (var b = function(){})PartiallyVariable hoisted as undefined, not the function

Key Rules

// ❌ Function Expression — NOT fully hoisted
b();  // TypeError: b is not a function
var b = function() {
    console.log("Hello");
}

// ❌ Arrow Function assigned to var — same issue
c();  // TypeError: c is not a function
var c = () => console.log("Hi");

// ✅ Function Declaration — fully hoisted
a();  // Works!
function a() {
    console.log("I'm hoisted!");
}

Interview Tip: "Hoisting is not about physically moving code. During the creation phase, JS allocates memory for variables (undefined) and functions (entire code) before executing anything."

3. How Functions Work & Variable Environment

Each function invocation creates its own Execution Context with its own Variable Environment.

var x = 1;
a();
b();
console.log(x); // 1

function a() {
    var x = 10;
    console.log(x); // 10 — looks in its OWN Variable Environment
}

function b() {
    var x = 100;
    console.log(x); // 100 — looks in its OWN Variable Environment
}

Step-by-Step Execution

  1. Global EC createdx = undefined, functions a and b stored in memory
  2. x = 1 → Global x updated
  3. a() called → New EC pushed onto call stack
    • Memory phase: local x = undefined
    • Execution: x = 10, logs 10
    • EC popped off stack
  4. b() called → New EC pushed
    • Memory phase: local x = undefined
    • Execution: x = 100, logs 100
    • EC popped off stack
  5. console.log(x) → Looks in Global EC → x = 1

Key Insight: Each function has its own independent variable environment. The var x inside a() is completely separate from var x in b() and the global x.

4. The Scope Chain & Lexical Environment

What is Lexical Environment?

Lexical Environment = Local Memory + Reference to Parent's Lexical Environment

"Lexical" means "in order" or "in hierarchy" — where the code is physically written determines its scope.

function a() {
    var b = 10;
    c();
    function c() {
        console.log(b); // 10 — found via scope chain
    }
}
a();

The Scope Chain

When a variable is accessed, JS looks for it in this order:

c()'s Local Memory → a()'s Local Memory → Global Memory → null

This chain of lexical environments is called the Scope Chain.

Interview Answer: "The scope chain is the chain of lexical environments. When JS can't find a variable in the current scope, it traverses up the scope chain until it finds it or reaches the global scope (where it returns ReferenceError if not found)."

5. let, const & the Temporal Dead Zone (TDZ)

Temporal Dead Zone (TDZ)

The TDZ is the time between when a let/const variable is hoisted and when it is initialized.

console.log(a); // ❌ ReferenceError: Cannot access 'a' before initialization
console.log(b); // undefined (var is hoisted and initialized with undefined)

let a = 10;
var b = 100;

Key Differences

Featurevarletconst
Hoisted?✅ Yes✅ Yes (in TDZ)✅ Yes (in TDZ)
Initial valueundefined❌ Not initialized❌ Not initialized
ScopeFunction scopeBlock scopeBlock scope
Stored inGlobal object (window)Separate memory spaceSeparate memory space
Re-declaration✅ Allowed❌ SyntaxError❌ SyntaxError
Re-assignment✅ Allowed✅ Allowed❌ TypeError
Must initialize at declaration?NoNo✅ Yes

Types of Errors Related to Hoisting

// 1. ReferenceError — accessing let/const in TDZ
console.log(a);  // ReferenceError: Cannot access 'a' before initialization
let a = 10;

// 2. SyntaxError — const without initialization
const c;         // SyntaxError: Missing initializer in const declaration

// 3. TypeError — reassigning const
const d = 100;
d = 200;         // TypeError: Assignment to constant variable

Best Practice: Use const by default. Use let when you need to reassign. Avoid var.

6. Block Scope & Shadowing

What is a Block?

A block {} groups multiple statements into one. Used wherever JS expects a single statement.

if (true) {
    // This is a block — groups multiple statements
    var a = 10;
    let b = 20;
    const c = 30;
}

Block Scope

{
    var a = 10;    // Stored in Global scope
    let b = 20;    // Stored in Block scope (separate memory)
    const c = 30;  // Stored in Block scope (separate memory)
    console.log(a); // 10 ✅
    console.log(b); // 20 ✅
    console.log(c); // 30 ✅
}
console.log(a); // 10 ✅ (var is globally scoped)
console.log(b); // ❌ ReferenceError (block scoped — gone!)
console.log(c); // ❌ ReferenceError (block scoped — gone!)

Shadowing

When a variable inside a block has the same name as one in the outer scope:

// var shadows var — MODIFIES the outer variable (same memory)
var a = 100;
{
    var a = 10;
    console.log(a); // 10
}
console.log(a); // 10 ⚠️ MODIFIED! Both point to same memory

// let shadows let — DOES NOT modify (different memory spaces)
let b = 100;
{
    let b = 20;
    console.log(b); // 20
}
console.log(b); // 100 ✅ Unchanged

// const shadows const — same as let
const c = 100;
{
    const c = 30;
    console.log(c); // 30
}
console.log(c); // 100 ✅ Unchanged

Illegal Shadowing

// ❌ Cannot shadow let with var (var crosses block boundary)
let a = 20;
{
    var a = 20; // SyntaxError: Identifier 'a' has already been declared
}

// ✅ CAN shadow var with let (let stays inside the block)
var b = 20;
{
    let b = 30; // This is fine
}

Rule of Thumb: var shadowing modifies the original. let/const shadowing creates a new variable. You cannot shadow let with var, but var with let is fine.

7. Closures

What is a Closure?

A closure is a function bundled together with its lexical environment.

In other words, a closure gives an inner function access to the outer function's scope — even after the outer function has returned.

function x() {
    var a = 7;
    function y() {
        console.log(a); // 7 — y "closes over" variable a
    }
    y();
}
x();

Closures in Action — Returning Functions

function x() {
    var a = 7;
    function y() {
        console.log(a);
    }
    return y;  // returning the function y
}

var z = x();    // x() finishes, its EC is popped off the stack
console.log(z); // [Function: y]
z();            // 7 ✅ — still has access to 'a'!

Why does this work?

  • When y is returned, it's returned along with its closure (its lexical environment)
  • Even after x() has been removed from the call stack, z() remembers variable a
  • The garbage collector doesn't clean up variables that are part of a closure

Closures Remember References, NOT Values

function o_f() {
    var a = 25;

    function i_f() {
        console.log(a);
    }

    a = 200;  // update AFTER defining i_f
    return i_f;
}

var getVal = o_f();
getVal(); // 200 (NOT 25!) — closure holds REFERENCE to 'a', not a copy

Critical Interview Point: JS does NOT copy the variable into the closure. It keeps a reference to the variable. So if the variable changes before the closure is called, the closure sees the updated value.

Key Points About Closures

  1. Functions in JS form closures
  2. A closure = function + its lexical environment
  3. When a function is returned, it maintains reference to its outer scope
  4. Closures remember references, not values (the variable, not a snapshot)
  5. The garbage collector doesn't collect variables referenced by closures

8. setTimeout + Closures — Classic Interview Question

The Problem

function x() {
    for (var i = 1; i <= 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
    }
    console.log("Namaste JavaScript");
}
x();

Expected: Namaste JavaScript, then 1, 2, 3, 4, 5 Actual: Namaste JavaScript, then 6, 6, 6, 6, 6 😱

Why?

  • var is function scoped — there's only ONE i shared across all iterations
  • All 5 setTimeout callbacks form closures over the same i
  • By the time callbacks execute, the loop has finished and i = 6

Solution 1: Use let (Block Scope)

function x() {
    for (let i = 1; i <= 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
    }
}
x();
// Output: 1, 2, 3, 4, 5 ✅

let creates a new variable for each iteration. Each callback closes over its own copy.

Solution 2: Use Closure with var (Without let)

function x() {
    for (var i = 1; i <= 5; i++) {
        function close(x) {
            setTimeout(function () {
                console.log(x);
            }, x * 1000);
        }
        close(i); // Pass current value — creates a new scope with a copy
    }
}
x();
// Output: 1, 2, 3, 4, 5 ✅

By passing i as an argument, each call to close() creates a new scope with its own x.

ConceptExplanation
var in loopSingle variable, shared by all iterations
let in loopNew variable per iteration (block scope)
ClosureFunction remembers its lexical environment
setTimeoutCallback placed in callback queue, executes later

9. Closures — Interview Questions & Patterns

Common Interview Answer

"A closure is a function bundled together with its lexical environment. A function along with its lexical scope forms a closure."

Uses of Closures

  • Module Design Pattern
  • Currying
  • Functions like once (execute only once)
  • Memoization
  • Maintaining state in async world
  • setTimeout callbacks
  • Iterators
  • Data hiding & encapsulation

Data Hiding / Encapsulation

function counter() {
    var count = 0;  // PRIVATE — hidden from outside

    return function incrementCounter() {
        count++;
        console.log(count);
    }
}

var counter1 = counter();
counter1(); // 1
counter1(); // 2

var counter2 = counter();
counter2(); // 1 (separate instance, separate count!)
counter2(); // 2

Constructor Function with Multiple Methods

function Counter() {
    var count = 0;  // private variable

    this.incrementCounter = function () {
        count++;
        console.log(count);
    }

    this.decrementCounter = function () {
        count--;
        console.log(count);
    }
}

var counter = new Counter();
counter.incrementCounter(); // 1
counter.incrementCounter(); // 2
counter.decrementCounter(); // 1

Disadvantages of Closures

  • Over consumption of memory — variables are not garbage collected as long as the closure exists
  • Memory leaks if closures are not handled properly
  • Can lead to unexpected behavior if you don't understand how closures reference variables

Note: Modern browsers (V8 engine) are smart about garbage collection — unused variables in a closure that are never referenced by the inner function are garbage collected. More on this in the Garbage Collector section further down.

10. First Class Functions & Function Types

Function Statement (Function Declaration)

function a() {
    console.log("a called");
}
a(); // ✅ Can be called before declaration (hoisted)

Function Expression

var b = function () {
    console.log("b called");
}
b(); // ✅ Works only after this line

Difference: Hoisting!

a(); // ✅ Works — Function Declaration is fully hoisted
b(); // ❌ TypeError: b is not a function — only the var is hoisted as undefined

function a() { console.log("a"); }
var b = function () { console.log("b"); }

Anonymous Function

A function without a name. Cannot be used as a standalone statement:

// ❌ SyntaxError
function () { }

// ✅ Used as a value (in expressions)
var x = function () { console.log("anonymous"); }
setTimeout(function () { console.log("callback"); }, 1000);

Named Function Expression

var c = function xyz() {
    console.log("c called");
    // console.log(xyz); // ✅ accessible INSIDE the function
}
c();    // ✅ Works
// xyz(); // ❌ ReferenceError — xyz is NOT accessible outside

Parameters vs Arguments

function greet(param1, param2) {  // param1, param2 = PARAMETERS (labels)
    console.log(param1, param2);
}
greet("Hello", "World");          // "Hello", "World" = ARGUMENTS (values)

First Class Functions (First Class Citizens)

The ability to use functions as values is called First Class Functions.

Functions can be:

  1. Assigned to variables
  2. Passed as arguments to other functions
  3. Returned from other functions
// Passing function as argument
var greet = function (fn) {
    fn();
}
greet(function () { console.log("Hello!"); });

// Returning a function
var outer = function () {
    return function () {
        console.log("returned function");
    }
}
outer()(); // "returned function"

11. Callback Functions

What is a Callback Function?

A function passed as an argument to another function, to be executed later.

function x(y) {
    console.log("x");
    y();  // calling the callback
}

x(function y() {
    console.log("y");
});
// Output: x, y

Callback functions give JavaScript the power of asynchronous programming, even though JS is synchronous and single-threaded.

Event Listeners + Closures

function attachEventListeners() {
    let count = 0;  // closure variable

    document.getElementById("clickMe")
        .addEventListener("click", function xyz() {
            console.log("Button clicked", ++count);
            // count is maintained through closure!
        });
}
attachEventListeners();

Why Remove Event Listeners?

  • Event listeners are heavy — they consume memory
  • The closure holds references to variables, preventing garbage collection
  • Even when the call stack is empty, the listener's closure keeps variables in memory
  • Always remove event listeners when no longer needed to prevent memory leaks

12. Event Loop, Callback Queue & Microtask Queue

The Big Picture

The browser provides Web APIs that are NOT part of JS itself:

  • setTimeout, setInterval
  • DOM APIs (document.getElementById, etc.)
  • fetch()
  • localStorage, console, location

How It All Works

┌─────────────┐     ┌──────────────┐     ┌────────────────┐
│  Call Stack  │ ←── │  Event Loop  │ ←── │ Microtask Queue│ (Higher Priority)
│             │     │              │     │ (Promises,     │
│             │     │              │     │  MutationObs.) │
│             │     │              │ ←── │ Callback Queue │ (Lower Priority)
│             │     │              │     │ (setTimeout,   │
│             │     │              │     │  setInterval)  │
└─────────────┘     └──────────────┘     └────────────────┘

   ┌──────────┐
   │ Web APIs │  (setTimeout timers, fetch requests, DOM events)
   └──────────┘

Event Loop's Job

The Event Loop has one simple job: continuously monitor the Call Stack and the queues.

  • If the Call Stack is empty, push the first callback from the queue to the stack
  • Microtask Queue has higher priority than Callback Queue

Example

console.log("Start");

setTimeout(function cb() {
    console.log("Callback");       // → Callback Queue
}, 5000);

fetch("https://api.netflix.com")
    .then(function cbf() {
        console.log("CB Netflix"); // → Microtask Queue
    });

console.log("End");

Output:

Start
End
CB Netflix    ← Microtask Queue (higher priority)
Callback      ← Callback Queue (after 5 seconds)

Microtask Queue vs Callback Queue

QueueContainsPriority
Microtask QueuePromise callbacks (.then, .catch), MutationObserver🔴 Higher
Callback Queue (Task Queue)setTimeout, setInterval, DOM events🔵 Lower

Starvation of Callback Queue

If the Microtask Queue keeps getting new tasks, the Callback Queue will starve — its callbacks may never execute because the Event Loop always prioritizes microtasks.

13. Trust Issues with setTimeout

setTimeout is NOT a Guarantee!

The timer in setTimeout is the MINIMUM wait time, not an exact time.

console.log("Start");

setTimeout(function () {
    console.log("Callback");
}, 0);  // Even 0ms is not instant!

console.log("End");

Output:

Start
End
Callback   ← Even with 0ms delay, it goes through the event loop!

Why?

  1. setTimeout registers the callback with the Web API timer
  2. After the timer expires, the callback is placed in the Callback Queue
  3. The Event Loop waits for the Call Stack to be empty
  4. Only then is the callback pushed to the Call Stack

Proof: Blocking Code Delays setTimeout

console.log("Start");

setTimeout(() => {
    console.log("Timeout");
}, 1000);

// Blocking code — keeps the call stack busy
for (let i = 0; i < 1e9; i++) {}

console.log("End");

Output:

Start
End       ← after the blocking loop finishes
Timeout   ← delayed way beyond 1 second!

The for loop blocks the call stack. Even though the timer expired at 1 second, the callback can't execute until the stack is free.

The Concurrency Model

Call Stack → Web APIs → Callback Queue → Event Loop → Call Stack

Interview Tip: "setTimeout(fn, 5000) doesn't guarantee execution at exactly 5 seconds. It guarantees the callback won't execute BEFORE 5 seconds. The actual time depends on how busy the call stack is."

JavaScript is Concurrent, Not Multi-threaded

  • JavaScript is single-threaded (one main call stack)
  • But it handles multiple tasks using concurrency via:
    • Event Loop
    • Callbacks / Promises / Async-Await
    • Web APIs (browser) / libuv (Node.js)
  • It executes synchronous code first, then processes async callbacks

14. Higher-Order Functions & Functional Programming

What is a Higher-Order Function?

A function that either:

  • Takes another function as an argument, OR
  • Returns a function

The Problem — Code Repetition

const radius = [3, 1, 2, 4];

// ❌ Repetitive code — only the formula changes
const calculateArea = function (radius) {
    const output = [];
    for (let i = 0; i < radius.length; i++) {
        output.push(Math.PI * radius[i] * radius[i]);
    }
    return output;
}

const calculateCircumference = function (radius) {
    const output = [];
    for (let i = 0; i < radius.length; i++) {
        output.push(2 * Math.PI * radius[i]);
    }
    return output;
}

The Solution — Higher-Order Functions

const radius = [3, 1, 2, 4];

// Extract the logic into small, reusable functions
const area = function (radius) {
    return Math.PI * radius * radius;
}

const circumference = function (radius) {
    return 2 * Math.PI * radius;
}

const diameter = function (radius) {
    return 2 * radius;
}

// Generic Higher-Order Function
const calculate = function (radiusArr, logic) {
    const output = [];
    for (let i = 0; i < radiusArr.length; i++) {
        output.push(logic(radiusArr[i]));
    }
    return output;
}

console.log(calculate(radius, area));           // Areas
console.log(calculate(radius, circumference));  // Circumferences
console.log(calculate(radius, diameter));       // Diameters

Key Insight: Our calculate function is essentially what Array.prototype.map does!

console.log(radius.map(area));  // Same result as calculate(radius, area)

15. map, filter & reduce

map — Transform Each Element

Returns a new array with each element transformed by the callback.

const arr = [5, 1, 3, 2, 6];

// Double each element
const doubled = arr.map(function (x) {
    return x * 2;
});
console.log(doubled); // [10, 2, 6, 4, 12]

// Triple (arrow function)
const tripled = arr.map((x) => x * 3);
console.log(tripled); // [15, 3, 9, 6, 18]

// Convert to binary
const binary = arr.map((x) => x.toString(2));
console.log(binary); // ['101', '1', '11', '10', '110']

filter — Keep Elements That Match

Returns a new array with only elements that pass the condition.

const arr = [5, 1, 3, 2, 6];

const oddValues = arr.filter((x) => x % 2 !== 0);
console.log(oddValues); // [5, 1, 3]

const evenValues = arr.filter((x) => x % 2 === 0);
console.log(evenValues); // [2, 6]

const greaterThan4 = arr.filter((x) => x > 4);
console.log(greaterThan4); // [5, 6]

reduce — Reduce to a Single Value

Takes a callback with accumulator and current value, plus an initial value.

const arr = [5, 1, 3, 2, 6];

// Sum of all elements
const sum = arr.reduce(function (acc, curr) {
    acc = acc + curr;
    return acc;
}, 0);  // 0 is the initial value
console.log(sum); // 17

// Max value
const max = arr.reduce(function (acc, curr) {
    if (curr > acc) {
        acc = curr;
    }
    return acc;
}, 0);
console.log(max); // 6

Real-World Example — Chaining map, filter, reduce

const users = [
    { firstName: "Sumit",  lastName: "Bera",  age: 26 },
    { firstName: "Akshay", lastName: "Saini", age: 26 },
    { firstName: "Donald", lastName: "Trump", age: 75 },
    { firstName: "Elon",   lastName: "Musk",  age: 50 },
];

// Get full names of users under 30
const output = users
    .filter((user) => user.age < 30)
    .map((user) => user.firstName + " " + user.lastName);
console.log(output); // ["Sumit Bera", "Akshay Saini"]

// Same thing using reduce
const output2 = users.reduce(function (acc, curr) {
    if (curr.age < 30) {
        acc.push(curr.firstName + " " + curr.lastName);
    }
    return acc;
}, []);
console.log(output2); // ["Sumit Bera", "Akshay Saini"]

// Count users by age
const ageCount = users.reduce(function (acc, curr) {
    if (acc[curr.age]) {
        acc[curr.age] = ++acc[curr.age];
    } else {
        acc[curr.age] = 1;
    }
    return acc;
}, {});
console.log(ageCount); // { 26: 2, 75: 1, 50: 1 }

Quick Reference

MethodPurposeReturnsCallback Signature
mapTransform each elementNew array (same length)(element, index, array)
filterKeep matching elementsNew array (≤ length)(element, index, array) → boolean
reduceReduce to single valueAny value(accumulator, current, index, array)

16. Callback Hell & Inversion of Control

What is Callback Hell?

When callbacks are nested inside callbacks, creating a pyramid of doom:

const cart = ["shoes", "pants", "kurta"];

api.createOrder(cart, function () {
    api.proceedToPayment(function () {
        api.showOrderSummary(function () {
            api.updateWallet();
        });
    });
});

The code grows horizontally instead of vertically, becoming unreadable and unmaintainable.

What is Inversion of Control?

When you pass a callback, you're giving control of your code to another function. You lose control over:

  1. ❓ Will the callback be called at all?
  2. ❓ Will it be called with the correct data?
  3. ❓ Will it be called only once? (Or multiple times?)

Real-World Analogy

Imagine ordering food at a restaurant. You give your order (callback) to the waiter (API). You trust the waiter to bring the right food, at the right time, only once. But what if the waiter forgets? Brings wrong food? Delivers twice?

Solution → Promises

The next section covers how Promises fix exactly this.

17. Promises

What is a Promise?

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation.

Before Promises vs After Promises

const cart = ["shoes", "pants", "kurta"];

// ❌ Before: Callback (Inversion of Control)
createOrder(cart, function (orderId) {
    proceedToPayment(orderId);
});

// ✅ After: Promise (We keep control)
const promise = createOrder(cart);
promise.then(function (orderId) {
    proceedToPayment(orderId);
});

Promise Object

// Promise States
{
    promiseState: "pending" | "fulfilled" | "rejected",
    promiseResult: undefined | data | error
}

Two Key Properties of Promises

  1. Immutable — Once resolved/rejected, the state and result cannot change
  2. Guaranteed execution.then() will be called exactly once when fulfilled

Using fetch (returns a Promise)

const GITHUB_API = "https://api.github.com/users/berasumit";

const user = fetch(GITHUB_API);
console.log(user); // Promise { <pending> }

user.then(function (data) {
    console.log(data); // Response object
});

// To get JSON data:
user.then((response) => response.json())
    .then((data) => console.log(data));

Promise Chaining

createOrder(cart)
    .then((orderId) => proceedToPayment(orderId))
    .then((paymentInfo) => showOrderSummary(paymentInfo))
    .then((paymentInfo) => updateWalletBalance(paymentInfo));

Key Rule: Always return from .then() to pass data to the next .then() in the chain.

18. Creating Promises, Chaining & Error Handling

Creating a Promise (Producer)

function createOrder(cart) {
    const pr = new Promise(function (resolve, reject) {
        // Validate cart
        if (!validateCart(cart)) {
            const err = new Error("Cart is not valid");
            reject(err);
        }

        // Create order
        const orderId = "12345";
        if (orderId) {
            setTimeout(function () {
                resolve(orderId);  // ✅ Success
            }, 5000);
        }
    });
    return pr;
}

function validateCart(cart) {
    return true;
}

Consuming a Promise

const promise = createOrder(cart);

promise
    .then(function (orderId) {
        console.log(orderId);
        return orderId;
    })
    .then(function (orderId) {
        return proceedToPayment(orderId);
    })
    .then(function (paymentInfo) {
        console.log(paymentInfo);
    })
    .catch(function (err) {
        console.log(err.message);
    })
    .then(function () {
        console.log("No matter what, this will execute");
    });

Error Handling with .catch()

function proceedToPayment(orderId) {
    return new Promise(function (resolve, reject) {
        resolve("Payment Successful");
    });
}

.catch() Placement Matters!

PlacementBehavior
.catch() at the endHandles errors from all .then() above it
.catch() in the middleHandles errors only from .then() above it; .then() below still executes
promise
    .then(/* step 1 */)
    .then(/* step 2 */)
    .catch(/* handles errors from step 1 & 2 */)
    .then(/* step 3 — STILL EXECUTES even if catch triggered */)

Producer vs Consumer

RoleDescription
ProducerCreates the promise using new Promise(resolve, reject)
ConsumerUses .then(), .catch(), .finally() to handle the result

19. async / await

What is async?

  • A keyword placed before a function to make it asynchronous
  • An async function always returns a Promise
async function getData() {
    return "Namaste";
}

const dataPromise = getData();
console.log(dataPromise); // Promise {<fulfilled>: 'Namaste'}

dataPromise.then((res) => console.log(res)); // "Namaste"

What is await?

  • Can only be used inside an async function
  • Pauses execution until the awaited promise resolves or rejects

How async/await Works Behind the Scenes

const p = new Promise(function (resolve, reject) {
    setTimeout(function () {
        resolve("Promise Resolved Value!!");
    }, 10000);
});

async function handlePromise() {
    console.log("Hello World");

    const val = await p;
    console.log("Namaste JavaScript");
    console.log(val);

    const val2 = await p;
    console.log("Namaste JavaScript 2");
    console.log(val2);
}

handlePromise();

Output:

Hello World                    ← Immediately
Namaste JavaScript             ← After 10 seconds (all at once)
Promise Resolved Value!!
Namaste JavaScript 2
Promise Resolved Value!!

Critical Behind-the-Scenes Behavior

  1. The async function is suspended when it hits await (not blocked!)
  2. The Call Stack is NOT blocked — the function is removed from the stack
  3. When the promise resolves, the function is put back on the call stack from where it was suspended
  4. This is NOT the same as blocking — other code continues to execute

Two Promises with Different Timers

const p1 = new Promise((resolve) => setTimeout(() => resolve("P1"), 10000)); // 10s
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2"), 5000));  // 5s

async function handlePromise() {
    console.log("Start");

    const val = await p1;   // waits for p1 (10s)
    console.log("P1:", val);

    const val2 = await p2;  // p2 already resolved at 5s, logs immediately
    console.log("P2:", val2);
}
handlePromise();

Output:

Start                          ← Immediately
P1: P1                         ← After 10 seconds
P2: P2                         ← Immediately after P1 (already resolved)

Both timers start simultaneously. await is sequential — it waits for each in order, but if a later promise resolved earlier, it logs instantly.

Real-World: fetch with async/await

const API_URL = "https://api.github.com/users/berasumit";

async function handleFetch() {
    try {
        const response = await fetch(API_URL);     // await #1: fetch returns a promise
        const jsonData = await response.json();     // await #2: .json() also returns a promise
        console.log(jsonData);
    } catch (err) {
        console.log(err);
    }
}
handleFetch();

Error Handling — Two Approaches

// Approach 1: try/catch (inside the function)
async function handleFetch() {
    try {
        const data = await fetch(API_URL);
        const json = await data.json();
        console.log(json);
    } catch (err) {
        console.log(err);
    }
}

// Approach 2: .catch() (outside the function)
async function handleFetch() {
    const data = await fetch(API_URL);
    const json = await data.json();
    console.log(json);
}
handleFetch().catch((err) => console.log(err));

fetch — Behind the Scenes

  1. fetch() returns a Promise
  2. The promise resolves with a Response object (a readable stream)
  3. .json() converts the Response to a JavaScript object
  4. .json() also returns a Promise → need two awaits

20. Promise APIs — all, allSettled, race, any

Promise.all()

Waits for all promises to resolve. Fail-fast on first rejection.

const p1 = new Promise((resolve) => setTimeout(() => resolve("P1 Success"), 3000));
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2 Success"), 1000));
const p3 = new Promise((resolve) => setTimeout(() => resolve("P3 Success"), 2000));

Promise.all([p1, p2, p3])
    .then((results) => console.log(results))
    // ["P1 Success", "P2 Success", "P3 Success"] (after 3s)
    .catch((err) => console.error(err));
    // If any ONE rejects → immediately returns that error

Promise.allSettled()

Waits for all promises to settle (resolve OR reject). Never fails fast.

Promise.allSettled([p1, p2, p3])
    .then((results) => console.log(results));

// Output:
// [
//   { status: 'fulfilled', value: 'P1 Success' },
//   { status: 'rejected',  reason: 'P2 Fail' },
//   { status: 'fulfilled', value: 'P3 Success' }
// ]

Promise.race()

Returns the result of the first settled promise (resolved OR rejected).

Promise.race([p1, p2, p3])
    .then((result) => console.log(result))   // "P2 Success" (fastest at 1s)
    .catch((err) => console.error(err));

Promise.any()

Returns the result of the first resolved (successful) promise. Ignores rejections.

Promise.any([p1, p2, p3])
    .then((result) => console.log(result))   // First success
    .catch((err) => {
        console.error(err);         // AggregateError (only if ALL reject)
        console.error(err.errors);  // Array of all rejection reasons
    });

Summary Table

APIWaits ForFails on First Reject?Returns
Promise.all()All to resolve✅ Yes (fail-fast)Array of values
Promise.allSettled()All to settle❌ NoArray of {status, value/reason}
Promise.race()First to settleN/A (first wins)First settled value or error
Promise.any()First to resolve❌ No (ignores rejects)First resolved value

21. The this Keyword

this in Different Contexts

1. Global Space

console.log(this); // window (browser) | global (Node.js)

2. Inside a Regular Function

function x() {
    console.log(this);
}
x();
// Non-strict mode → window (due to "this substitution")
// Strict mode     → undefined

This Substitution: In non-strict mode, if this is undefined or null, JS automatically replaces it with the global object.

3. How this Depends on How Function is Called

x();        // this = undefined (strict) | window (non-strict)
window.x(); // this = window (x is called as a method of window)

4. Inside an Object's Method

const obj = {
    a: 10,
    x: function () {
        console.log(this);   // { a: 10, x: f } → the object itself
        console.log(this.a); // 10
    }
}
obj.x();

5. call / apply / bind — Explicit Binding

const student = { name: "Sumit" };
const student2 = { name: "Akshay" };

function printName() {
    console.log(this.name);
}

printName.call(student);  // "Sumit"
printName.call(student2); // "Akshay"

6. Arrow Functions — Lexical this

Arrow functions don't have their own this. They inherit from the enclosing lexical scope.

const obj = {
    a: 10,
    x: () => {
        console.log(this); // window! (inherits from global scope)
    }
}
obj.x();

// Nested: Arrow inside a regular method
const obj2 = {
    a: 10,
    x: function () {
        const y = () => {
            console.log(this); // { a: 10, x: f } → obj2!
            // Arrow inherits 'this' from x(), which is obj2
        }
        y();
    }
}
obj2.x();

7. Inside DOM Elements

<button onclick="alert(this)">Click Me</button>
<!-- this → the button element -->

8. Inside a Class

class User {
    constructor(name) {
        this.name = name; // this = instance of User
    }
    getName() {
        console.log(this.name);
    }
}
const user = new User("Sumit");
user.getName(); // "Sumit"

Complete this Summary

Contextthis Value
Global spaceGlobal object (window / global)
Regular function (strict)undefined
Regular function (non-strict)Global object (this substitution)
Object methodThe object
call / apply / bindExplicitly set object
Arrow functionInherited from enclosing lexical scope
DOM event handlerThe HTML element
ClassInstance of the class

22. call, apply & bind

Function Borrowing with call

let name = {
    firstName: "Sumit",
    lastName: "Bera",
    printFullName: function () {
        console.log(this.firstName + " " + this.lastName);
    }
}

let name2 = {
    firstName: "Sachin",
    lastName: "Tendulkar",
}

// Borrow the method — "this" now refers to name2
name.printFullName.call(name2); // "Sachin Tendulkar"

Standalone Function with call

let printFullName = function (hometown, state) {
    console.log(this.firstName + " " + this.lastName + " from " + hometown + ", " + state);
}

printFullName.call(name, "Bankura", "West Bengal");
// "Sumit Bera from Bankura, West Bengal"

printFullName.call(name2, "Mumbai", "Maharashtra");
// "Sachin Tendulkar from Mumbai, Maharashtra"

apply — Same as call, but arguments as array

printFullName.apply(name2, ["Mumbai", "Maharashtra"]);
// "Sachin Tendulkar from Mumbai, Maharashtra"

bind — Returns a new function (doesn't invoke immediately)

let printMyName = printFullName.bind(name, "Bankura", "West Bengal");
console.log(printMyName); // [Function: bound printFullName]
printMyName();            // "Sumit Bera from Bankura, West Bengal"

Key Differences

MethodInvokes Immediately?Arguments FormatReturns
call✅ YesIndividual argsResult of function
apply✅ YesArray of argsResult of function
bind❌ NoIndividual argsNew bound function

23. Polyfill for bind

A polyfill is code that provides functionality on older browsers that don't natively support it.

The Problem

let name = {
    firstName: "Sumit",
    lastName: "Bera",
}

let printName = function (hometown) {
    console.log(this.firstName + " " + this.lastName + " from " + hometown);
}

// Built-in bind
let printMyName = printName.bind(name, "Bankura");
printMyName(); // "Sumit Bera from Bankura"

Custom Polyfill for bind (Most Asked Interview Question)

Function.prototype.myBind = function (...args) {
    let obj = this;              // 'this' = the function myBind is called on (printName)
    let params = args.slice(1);  // All args except the first (context object)

    return function (...args2) {
        obj.apply(args[0], [...params, ...args2]);
        // args[0] = context (the object to bind)
        // params  = pre-filled arguments from myBind call
        // args2   = arguments passed when the bound function is called
    }
}

let printMyName2 = printName.myBind(name, "Bankura");
printMyName2(); // "Sumit Bera from Bankura"

How the Polyfill Works

  1. myBind is added to Function.prototype — available on all functions
  2. this inside myBind refers to the function it's called on
  3. args[0] is the context object (what this should be)
  4. args.slice(1) captures any pre-filled arguments
  5. Returns a new function that uses apply to call the original with the correct context

Common Use Case: Fixing this Loss

const obj = {
    name: "Sumit",
    print: function () {
        console.log(this.name);
    },
};

const fn = obj.print;
fn();  // undefined — 'this' is lost when method is extracted

// Fix with bind:
const fixedFn = obj.print.bind(obj);
fixedFn();  // "Sumit" ✅

24. Currying

Currying is a technique where a function with multiple arguments is transformed into a sequence of functions, each taking one argument at a time.

// Normal function
function add(a, b) {
    return a + b;
}
add(2, 3); // 5

// Curried version
function add(a) {
    return function (b) {
        return a + b;
    };
}
add(2)(3); // 5

Why Currying?

  • Partial application — pre-fill some arguments
  • Reusability — create specialized functions from general ones
  • Functional composition — chain small functions together
// Practical example: creating specialized functions
function multiply(a) {
    return function (b) {
        return a * b;
    };
}

const double = multiply(2);
const triple = multiply(3);

console.log(double(5));  // 10
console.log(triple(5));  // 15

Connection to Closures: Currying works because of closures. The inner function "remembers" the outer function's argument through its lexical scope.

25. Garbage Collector & Closures

How Garbage Collection Works with Closures

The garbage collector frees memory for variables that are no longer referenced. But closures can prevent this.

function a() {
    var x = 0;
    var z = 10;  // z is NOT used by the inner function

    return function b() {
        console.log(x);  // only x is referenced
    }
}

var y = a();
y();

Smart Garbage Collection (V8 Engine)

  • Modern browsers (V8 engine in Chrome) are smart about this
  • Variable z is NOT used by the closure, so V8 garbage collects it
  • Variable x is referenced, so it stays in memory
  • This is an optimization — older engines would keep both variables

Interview Tip: "Closures can cause memory issues if variables are held unnecessarily. Modern engines like V8 optimize by only keeping variables that the closure actually references. However, it's still good practice to be mindful of closure memory usage and remove event listeners when no longer needed."

Event Listeners & Memory

Event listeners are a common source of memory leaks because:

  1. They form closures over their enclosing scope
  2. They persist until explicitly removed
  3. Even when the call stack is empty, the closure variables stay in memory
function attachEventListeners() {
    let count = 0;
    const btn = document.getElementById("clickMe");

    function cb() {
        console.log("clicked", count++);
    }

    btn.addEventListener("click", cb);

    // ✅ Good practice: remove when done
    // btn.removeEventListener("click", cb);
}

26. Shortest JS Program & Global Object

Shortest JS Program

An empty file is the shortest valid JavaScript program. Even with no code, JS creates:

  1. A Global Execution Context
  2. The global object (window in browser, global in Node.js)
  3. The this keyword (pointing to the global object)
// Even in an empty file:
console.log(window);  // Window object (in browser)
console.log(this);    // Window object
console.log(this === window);  // true

Important Advice

// ❌ Never do this — valid code but terrible practice
var a = undefined;

// ✅ Let JavaScript handle undefined naturally
var a;
console.log(a);  // undefined (JS assigns this automatically)

undefined means the variable has been declared but not yet assigned a value. Never explicitly set a variable to undefined.

27. Quick Revision Cheat Sheet

Execution & Scope

  • Execution Context = Memory Component + Code Component
  • Call Stack manages execution order (LIFO)
  • Scope Chain = chain of Lexical Environments (local → parent → global)
  • Hoisting = memory allocated before execution (varundefined, functions → full code)
  • TDZ = time between hoisting and initialization for let/const

Variables

  • var → function scoped, hoisted with undefined, stored in global object
  • let → block scoped, hoisted in TDZ, separate memory space
  • const → block scoped, must initialize, cannot reassign

Functions

  • Function Declaration → fully hoisted
  • Function Expression → variable hoisted as undefined
  • Arrow Functions → no own this, inherit from lexical scope
  • First Class Functions → can be assigned, passed, and returned

Closures

  • Closure = Function + Lexical Environment
  • Used for: data hiding, memoization, currying, module pattern
  • Pitfall: var in loops with setTimeout (use let or IIFE)

Async JavaScript

  • Callback QueuesetTimeout, setInterval, DOM events
  • Microtask Queue → Promises, MutationObserver (higher priority)
  • Event Loop → moves callbacks from queue to call stack when stack is empty
  • setTimeout(fn, 0) still goes through the event loop

Promises

  • States: pendingfulfilled | rejected
  • .then() for success, .catch() for errors, .finally() for cleanup
  • Always return in .then() for chaining

Promise APIs

  • Promise.all() → all succeed or fail-fast
  • Promise.allSettled() → wait for all, never fails
  • Promise.race() → first to settle wins
  • Promise.any() → first to succeed wins

async/await

  • async function always returns a Promise
  • await pauses execution (doesn't block call stack)
  • Error handling: try/catch or .catch() on the call

this Keyword

  • Global → window / global
  • Function (strict) → undefined
  • Function (non-strict) → global object
  • Object method → the object
  • Arrow function → inherits from parent scope
  • call/apply/bind → explicit binding

call / apply / bind

  • call(context, arg1, arg2) → invokes immediately
  • apply(context, [arg1, arg2]) → invokes immediately (array args)
  • bind(context, arg1) → returns new function

💡 Tip: For interviews, focus on closures, event loop, promises vs async/await, this keyword, and hoisting. These are the most frequently asked JavaScript concepts.