Deep Dive: JavaScript Core Concepts & Internals
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:
| Component | Also Known As | Contains |
|---|---|---|
| Memory Component | Variable Environment | Variables & functions stored as key-value pairs |
| Code Component | Thread of Execution | Code 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 Type | Hoisted? | Initial Value |
|---|---|---|
var | ✅ Yes | undefined |
function declaration | ✅ Yes | Entire function code |
let / const | ✅ Yes (but in TDZ) | ❌ Cannot access before declaration |
Function Expression (var b = function(){}) | Partially | Variable 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
- Global EC created →
x = undefined, functionsaandbstored in memory x = 1→ Globalxupdateda()called → New EC pushed onto call stack- Memory phase: local
x = undefined - Execution:
x = 10, logs10 - EC popped off stack
- Memory phase: local
b()called → New EC pushed- Memory phase: local
x = undefined - Execution:
x = 100, logs100 - EC popped off stack
- Memory phase: local
console.log(x)→ Looks in Global EC →x = 1
Key Insight: Each function has its own independent variable environment. The
var xinsidea()is completely separate fromvar xinb()and the globalx.
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
ReferenceErrorif 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
| Feature | var | let | const |
|---|---|---|---|
| Hoisted? | ✅ Yes | ✅ Yes (in TDZ) | ✅ Yes (in TDZ) |
| Initial value | undefined | ❌ Not initialized | ❌ Not initialized |
| Scope | Function scope | Block scope | Block scope |
| Stored in | Global object (window) | Separate memory space | Separate memory space |
| Re-declaration | ✅ Allowed | ❌ SyntaxError | ❌ SyntaxError |
| Re-assignment | ✅ Allowed | ✅ Allowed | ❌ TypeError |
| Must initialize at declaration? | No | No | ✅ 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
constby default. Useletwhen you need to reassign. Avoidvar.
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:
varshadowing modifies the original.let/constshadowing creates a new variable. You cannot shadowletwithvar, butvarwithletis 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
yis returned, it's returned along with its closure (its lexical environment) - Even after
x()has been removed from the call stack,z()remembers variablea - 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
- Functions in JS form closures
- A closure = function + its lexical environment
- When a function is returned, it maintains reference to its outer scope
- Closures remember references, not values (the variable, not a snapshot)
- 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?
varis function scoped — there's only ONEishared across all iterations- All 5
setTimeoutcallbacks form closures over the samei - 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.
| Concept | Explanation |
|---|---|
var in loop | Single variable, shared by all iterations |
let in loop | New variable per iteration (block scope) |
| Closure | Function remembers its lexical environment |
| setTimeout | Callback 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:
- ✅ Assigned to variables
- ✅ Passed as arguments to other functions
- ✅ 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
| Queue | Contains | Priority |
|---|---|---|
| Microtask Queue | Promise 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?
setTimeoutregisters the callback with the Web API timer- After the timer expires, the callback is placed in the Callback Queue
- The Event Loop waits for the Call Stack to be empty
- 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
calculatefunction is essentially whatArray.prototype.mapdoes!
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
| Method | Purpose | Returns | Callback Signature |
|---|---|---|---|
map | Transform each element | New array (same length) | (element, index, array) |
filter | Keep matching elements | New array (≤ length) | (element, index, array) → boolean |
reduce | Reduce to single value | Any 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:
- ❓ Will the callback be called at all?
- ❓ Will it be called with the correct data?
- ❓ 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
- Immutable — Once resolved/rejected, the state and result cannot change
- 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
returnfrom.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!
| Placement | Behavior |
|---|---|
.catch() at the end | Handles errors from all .then() above it |
.catch() in the middle | Handles 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
| Role | Description |
|---|---|
| Producer | Creates the promise using new Promise(resolve, reject) |
| Consumer | Uses .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
asyncfunction - 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
- The async function is suspended when it hits
await(not blocked!) - The Call Stack is NOT blocked — the function is removed from the stack
- When the promise resolves, the function is put back on the call stack from where it was suspended
- 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.
awaitis 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
fetch()returns a Promise- The promise resolves with a Response object (a readable stream)
.json()converts the Response to a JavaScript object.json()also returns a Promise → need twoawaits
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
| API | Waits For | Fails on First Reject? | Returns |
|---|---|---|---|
Promise.all() | All to resolve | ✅ Yes (fail-fast) | Array of values |
Promise.allSettled() | All to settle | ❌ No | Array of {status, value/reason} |
Promise.race() | First to settle | N/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
thisisundefinedornull, 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
| Context | this Value |
|---|---|
| Global space | Global object (window / global) |
| Regular function (strict) | undefined |
| Regular function (non-strict) | Global object (this substitution) |
| Object method | The object |
call / apply / bind | Explicitly set object |
| Arrow function | Inherited from enclosing lexical scope |
| DOM event handler | The HTML element |
| Class | Instance 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
| Method | Invokes Immediately? | Arguments Format | Returns |
|---|---|---|---|
call | ✅ Yes | Individual args | Result of function |
apply | ✅ Yes | Array of args | Result of function |
bind | ❌ No | Individual args | New 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
myBindis added toFunction.prototype— available on all functionsthisinsidemyBindrefers to the function it's called onargs[0]is the context object (whatthisshould be)args.slice(1)captures any pre-filled arguments- Returns a new function that uses
applyto 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
zis NOT used by the closure, so V8 garbage collects it - Variable
xis 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:
- They form closures over their enclosing scope
- They persist until explicitly removed
- 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:
- A Global Execution Context
- The global object (
windowin browser,globalin Node.js) - The
thiskeyword (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)
undefinedmeans the variable has been declared but not yet assigned a value. Never explicitly set a variable toundefined.
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 (
var→undefined, functions → full code) - TDZ = time between hoisting and initialization for
let/const
Variables
var→ function scoped, hoisted withundefined, stored in global objectlet→ block scoped, hoisted in TDZ, separate memory spaceconst→ 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:
varin loops withsetTimeout(useletor IIFE)
Async JavaScript
- Callback Queue →
setTimeout,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:
pending→fulfilled|rejected .then()for success,.catch()for errors,.finally()for cleanup- Always
returnin.then()for chaining
Promise APIs
Promise.all()→ all succeed or fail-fastPromise.allSettled()→ wait for all, never failsPromise.race()→ first to settle winsPromise.any()→ first to succeed wins
async/await
asyncfunction always returns a Promiseawaitpauses execution (doesn't block call stack)- Error handling:
try/catchor.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 immediatelyapply(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.