← Writing
The Editorial · Engineering

Namaste JavaScript, distilled

Issue 002Jun 29, 202653 min read

Clean revision notes for Akshay Saini's Namaste JavaScript — the foundations, stripped to the core. Execution contexts, the call stack, hoisting, scope and the scope chain, let/const and the TDZ, block scope, closures, functions, callbacks and callback hell, the event loop, the JS engine, higher-order functions, map/filter/reduce, promises, async/await, the Promise combinators, and the this keyword — in as few words as possible.

The issue

These are tight revision notes for the opening lectures of Akshay Saini's Namaste JavaScript — the free series that explains how JavaScript actually works under the hood. The goal here is the opposite of a textbook: the fewest words that still make it click. Read it before an interview, or whenever the fundamentals feel fuzzy.

How JavaScript runs

JS is synchronous (in a fixed order) and single-threaded (one command at a time). Everything happens inside an execution context — think of it as a sealed box holding the info about the code currently running.

That box has two parts. The memory component (also called the variable environment) stores every variable and function as a key-value pair. The code component (the thread of execution) runs the code one line at a time.

two-halves.jsJavaScript
var n = 2;
function square(a) {
  return a * a;
}

// Memory component  (key -> value)
//   n       -> 2
//   square  -> { full function code }
//
// Code component  (runs line by line)
//   line 1: n = 2
//   line 2: square stored, nothing to run yet
Execution ContextMemoryvariable environmentn2square{ … }Codethread of executionone line at a time
Fig 1One execution context, two halves: memory holds every variable and function as a key-value pair, while the code side runs the program one line at a time.AI-generated figure

Two phases, every time

When a program runs, JS creates a global execution context in two phases. The memory phase comes first: JS scans the whole file and reserves space — variables are set to undefined, functions store their entire code. Only then does the code execution phase run, top to bottom, filling in the real values.

context.jsJavaScript
var n = 2;
function square(a) {
  return a * a;
}
var square2 = square(n);
var square4 = square(4);

// Memory phase reserves:
//   n -> undefined, square -> {full code},
//   square2 -> undefined, square4 -> undefined
//
// Code execution phase then runs and fills in the real values:
//   n -> 2, square -> {full code},
//   square2 -> 4   (square(2)),
//   square4 -> 16  (square(4))

The call stack

Every function call creates its own execution context, which runs the same two phases. The call stack keeps track of where JS is: it pushes a context when a function is called and pops it when the function returns. When return is hit, control goes back to the calling line and that function's context is deleted. When the whole program finishes, the global context is popped off too and the stack is empty.

Call Stacksquare() ECGlobal ECpush on callpop on return
Fig 2Each call pushes a fresh execution context onto the stack and each return pops it off. The global context sits at the base and the currently running one sits on top.AI-generated figure

Hoisting

Hoisting is the ability to use variables and functions before they appear in the code, without an error. There's no magic — it's just the memory phase doing its job before any code runs. The rule: a var is available as undefined, and a function declaration is available in full.

source ordergetName();console.log(x);var x = 7;function getName(){…}phase 1memory before runxundefinedgetNamefn { … }
Fig 3Phase one walks the source and reserves memory before a single line runs: a var lands as undefined, a function declaration stores its whole body — which is exactly why both can be used early.AI-generated figure
hoisting.jsJavaScript
getName();         // Namaste JavaScript   (whole function is in memory)
console.log(x);    // undefined            (var, not yet assigned)
var x = 7;
function getName() {
  console.log("Namaste JavaScript");
}

Remove the declaration entirely and it's a different story — the name was never put in memory, so JS throws and execution stops:

not-defined.jsJavaScript
console.log(x);    // ReferenceError: x is not defined  -> execution stops
// 'x' was never declared, so it isn't in memory at all.

The one exception: function expressions

A function assigned to a variable behaves like a var, not a function. During the memory phase it's just undefined, so calling it early means calling undefined() — a TypeError.

expression.jsJavaScript
getName();   // Uncaught TypeError: getName is not a function

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

// getName is a var -> 'undefined' in memory -> undefined() throws.
// Move the call below the assignment and it works fine.

Every function gets its own memory

Because each function call spins up its own execution context, variables declared inside a function are local to it. Two functions can use the same name and never collide — each x lives in a separate box, and the global x is untouched.

scope.jsJavaScript
var x = 1;
a();              // 10
b();              // 100
console.log(x);   // 1   (global x is untouched)

function a() {
  var x = 10;     // local to a()
  console.log(x);
}
function b() {
  var x = 100;    // local to b()
  console.log(x);
}
Global ECx1a() ECx10b() ECx100same name, separate boxes
Fig 4The global context holds x = 1; calling a() and b() each create a fresh context with their own x. Same name, separate boxes — they never overwrite one another.AI-generated figure

The shortest program isn't empty

Even an empty file makes JS do real work: it still creates the global execution context. In the browser it also creates a global object — window — packed with built-in functions and variables, plus a this that points to it. At the global level, this === window. Any variable you declare in global scope gets attached to that global object.

global.jsJavaScript
var x = 10;
console.log(x);        // 10
console.log(this.x);   // 10   (this === window at the global level)
console.log(window.x); // 10   (globals attach to the global object)

undefined vs not defined

These look similar but mean different things. undefined is a real placeholder value: the variable was declared (so it exists in memory) but hasn't been assigned yet. not defined means the variable was never declared at all — so accessing it throws a ReferenceError.

undefined.jsJavaScript
console.log(x);   // undefined   (declared, not yet assigned)
var x = 25;
console.log(x);   // 25

console.log(a);   // ReferenceError: a is not defined  (never declared)

Scope chain and lexical environment

A function can use variables from its own scope and from every scope it sits inside, all the way up to global. That ladder of lookups is the scope chain. The formal version: a lexical environment is a function's local memory plus a reference to its parent's lexical environment — and "lexical" just means where the code is physically written.

scope-chain.jsJavaScript
function a() {
  c();
  function c() {
    console.log(b); // 10  -> not here, not in a(), found in global
  }
}
var b = 10;
a();

// A local b would win over the global one (it's found first).
// But the reverse never works:
function outer() {
  var secret = 42;
}
outer();
console.log(secret); // ReferenceError: secret is not defined
Globalb = 10a()c()console.log(b)
Fig 5c() looks for b, doesn't find it, so it walks outward — through a(), then to Global, where b = 10 lives. Inner scopes can reach outward; the global scope can never reach in.AI-generated figure

let, const, and the Temporal Dead Zone

let and const are hoisted too — but unlike var they aren't set to undefined. They sit in the Temporal Dead Zone (TDZ) from the moment they're hoisted until the line that assigns them. Touch them in that window and you get a ReferenceError, not undefined. They also don't attach to the global object.

tdz.jsJavaScript
console.log(a);  // ReferenceError: Cannot access 'a' before initialization
console.log(b);  // (var) would be undefined
let a = 10;
var b = 15;

// And they aren't attached to window / this:
console.log(window.b); // 15
console.log(window.a); // undefined  (let lives outside the global object)
let a = 10 · lifecycleTemporal Dead Zonehoistedaccess → ReferenceErrorinitializedusable
Fig 6A let is hoisted but parked in the Temporal Dead Zone — reading it there throws a ReferenceError. Only the line that assigns it ends the TDZ and makes it usable.AI-generated figure

Block scope and shadowing

A block is anything inside { }. let and const are block-scoped — they only exist inside the block they're declared in — while var ignores blocks entirely and leaks out.

block.jsJavaScript
{
  var a = 10;
  let b = 20;
  const c = 30;
}
console.log(a); // 10   (var leaked out of the block)
console.log(b); // ReferenceError: b is not defined

When an inner name matches an outer one, the inner declaration shadows the outer. With let/const the block keeps its own copy and the outer value survives; with var there's really just one variable, so the inner write changes the outer too. And you can't shadow a let with a var in the same scope — that's illegal shadowing.

shadowing.jsJavaScript
let a = 100;
{
  let a = 10;     // a separate, block-scoped 'a'
  console.log(a); // 10
}
console.log(a);   // 100  (outer 'a' untouched)

// Illegal shadowing — a var can't shadow a block-scoped let:
let x = 20;
if (true) {
  var x = 30;     // SyntaxError: Identifier 'x' has already been declared
}

Closures

A closure is a function bundled together with its lexical scope. Because lookups walk the scope chain, an inner function keeps a live reference to its outer function's variables — and holds onto them even after the outer function has returned.

closure.jsJavaScript
function x() {
  var a = 7;
  function y() {
    console.log(a); // y closes over a
  }
  return y;
}

var z = x();   // x() has finished and is gone...
z();           // 7   — but z still remembers a
x() has returnedx() scopea = 7function y()return yz = x()function y()a = 7remembered
Fig 7x() returns y, and y carries its lexical scope with it. Even after x() is gone, calling z() still reads a = 7 — the variable is kept alive by the closure.AI-generated figure

Interview classic: setTimeout in a loop

Time, tide, and JavaScript wait for none. The callback inside setTimeout forms a closure over its surroundings; setTimeout stores it with a timer and moves on without waiting. So this prints the string first, then the value after the delay:

settimeout.jsJavaScript
function x() {
  var i = 1;
  setTimeout(function () {
    console.log(i);
  }, 3000);
  console.log("Namaste JavaScript");
}
x();
// Namaste JavaScript   (immediately)
// 1                    (after 3 seconds)

Now the famous trap — print 1, 2, 3, 4, 5 one second apart. The obvious loop with var prints 6 five times instead:

loop-trap.jsJavaScript
function x() {
  for (var i = 1; i <= 5; i++) {
    setTimeout(function () {
      console.log(i);
    }, i * 1000);
  }
}
x();
// 6 6 6 6 6   — not 1 2 3 4 5
vari = 6one shared i6 6 6 6 6let123451 2 3 4 5a fresh i each loop
Fig 8With var there's a single i; all five callbacks close over that same reference, and by the time they fire the loop has finished and i is 6. With let, each iteration gets its own block-scoped i, so the callbacks capture 1 through 5.AI-generated figure

Why? Every callback closes over the same i — a reference, not a copy. The loop finishes long before the first timer fires, leaving i at 6. The clean fix is let, which is block-scoped, so each iteration creates a brand-new i for its callback to remember.

loop-fixed.jsJavaScript
for (let i = 1; i <= 5; i++) {
  setTimeout(function () {
    console.log(i);   // 1, 2, 3, 4, 5 — one per second
  }, i * 1000);
}

// Forced to use var? Wrap it so each call gets a fresh copy of i:
for (var j = 1; j <= 5; j++) {
  (function (j) {
    setTimeout(() => console.log(j), j * 1000);
  })(j);              // a new j per iteration — also 1..5
}

The many forms of a function

A function can be written several ways, and each form has a name worth knowing. The first split is statement vs expression. A function statement (declaration) is the named function foo() {} form; a function expression assigns a function to a variable. The difference that bites is hoisting: a declaration is fully hoisted and callable before its line, while an expression is just a varundefined until its line runs.

functions.jsJavaScript
a(); // "Hello A"  — declaration, hoisted fully
b(); // TypeError    — b is still undefined here

function a() { console.log("Hello A"); }   // statement
var b = function () { console.log("Hello B"); }; // expression

// A function with no name is anonymous; give the expression a name
// and it's a "named function expression":
var c = function ()    { /* anonymous */ };
var d = function named() { /* named */ };

Drop the name and you have an anonymous function. On its own it's a SyntaxError — a statement requires a name — so anonymous functions only appear where a function is used as a value (like the expression above). Give that expression a name and it becomes a named function expression, but the name lives only inside the function, not in the outer scope.

named-expression.jsJavaScript
var b = function xyz() {
  console.log("b called");
};
b();    // "b called"
xyz();  // ReferenceError: xyz is not defined  (name isn't in the outer scope)

// Higher-order functions: take a function in, or return one out.
function hof(fn) {
  return function () { return fn(); }; // returns a function
}
iife.jsJavaScript
// Define a function and run it on the spot — no name, no later call:
(function () {
  console.log("runs immediately");
})();

// Arrow form, same idea:
(() => {
  console.log("also immediate");
})();

// It can take arguments and return a value, too:
var sum = (function (a, b) {
  return a + b;
})(2, 3); // 5

Wrap a function in parentheses and call it on the spot and you have an Immediately Invoked Function Expression (IIFE). The parentheses turn the declaration into an expression, and the trailing () runs it the instant it's defined. The point is a private scope: variables declared inside an IIFE never leak to the outside — the classic way to avoid polluting the global namespace before block scope and modules existed.

Two more terms, often mixed up: parameters are the labels in the definition; arguments are the actual values you pass at call time. And whichever form they take, functions in JS are first-class citizens — you can pass them as arguments and return them from other functions, which is exactly what makes callbacks and closures possible.

Callbacks and event listeners

A callback is a function you pass into another function so it can be called later. It's how JavaScript — single-threaded by nature — does work that happens out of order: you hand setTimeout a function and a delay, the synchronous code runs first, and the callback fires afterwards.

callback.jsJavaScript
setTimeout(function () {
  console.log("timer");
}, 5000);

function x(y) {
  console.log("x");
  y();              // y is the callback, invoked inside x
}
x(function y() {
  console.log("y");
});

// Output:  x   y   timer   (timer comes last, after 5s)
execution orderx()y()now · synchronoussetTimeout waitstimerlater · after 5s
Fig 9x() and y() run immediately on the call stack; the setTimeout callback is set aside and runs later. The synchronous work never waits for the timer.AI-generated figure

Everything runs through the call stack, and there's only one. So a slow, synchronous operation blocks the main thread — the whole page freezes until it finishes. The fix is to push time-taking work through async APIs like setTimeout so the stack stays free.

Event listeners are just callbacks attached to DOM events. addEventListener registers a handler without overwriting existing ones, and that handler forms a closure — here over count — so it keeps state between clicks.

listener.jsJavaScript
function attachClickHandler() {
  let count = 0;
  document.getElementById("btn").addEventListener("click", function () {
    console.log("button clicked", ++count); // closes over count
  });
}
attachClickHandler();

The event loop, queues, and microtasks

The call stack has no timer — it runs whatever enters it, immediately. So how does setTimeout wait? The browser hands JS extra powers through Web APIs — setTimeout, fetch, the DOM, even console.log — reached via the global window object. Async callbacks are registered with those Web APIs, not parked on the stack.

event-loop.jsJavaScript
console.log("Start");
setTimeout(function cb() {
  console.log("timer");
}, 5000);
console.log("End");

// Start, End, timer   (timer is registered with a Web API, runs last)

Step by step: Start logs, setTimeout registers cb with the Web API and starts a 5s timer, then returns immediately; End logs and the global context pops. The timer ticks outside the engine. When it expires, cb can't jump straight back onto the stack — it lands in the callback queue and waits.

Call StackGECWeb APIssetTimeout()fetch()DOM · consoleregister callbackMicrotask Q · promiseshigher priorityCallback Q · timers, eventstasksEvent Loopstack empty → push
Fig 10Async callbacks are registered with the Web APIs, then queued when they're ready. The event loop watches the queues and, the moment the call stack is empty, pushes the next callback on. Promise callbacks use the microtask queue, which outranks the callback queue.AI-generated figure

The event loop is the gatekeeper: it constantly checks the queues and, the instant the call stack is empty, pushes the next callback on. Event listeners work the same way — the handler lives in the Web API environment until the event fires (or you remove it).

Promises are special. Their .then callbacks go to the microtask queue, which the event loop fully drains before it touches the callback queue — so promise work runs ahead of setTimeout and DOM callbacks (Mutation Observer callbacks are microtasks too). If microtasks keep scheduling more microtasks, the callback queue can be starved.

microtask.jsJavaScript
console.log("Start");
setTimeout(() => console.log("CB Timeout"), 0);   // callback queue
fetch("https://api.example.com").then(() => console.log("CB Promise")); // microtask
console.log("End");

// Start, End, CB Promise, CB Timeout
// the promise callback jumps ahead — microtasks beat the callback queue

Why setTimeout can't promise exactly 5 seconds

A 5-second setTimeout means at least 5 seconds, not exactly 5. The callback can only run once the call stack is empty — so if your synchronous code keeps the stack busy for 10 seconds, the timer expires at 5s and the callback waits in the queue until the stack finally clears at 10s.

trust.jsJavaScript
console.log("Start");
setTimeout(function cb() {
  console.log("Callback");
}, 5000);
console.log("End");

// ...imagine a million lines of synchronous code that take ~10s here...
// The timer expires at 5s, but cb runs only after the stack is empty (~10s).
setTimeout(cb, 5000)call stack busy ~10scb waits5s · timer done10s · cb runs
Fig 11The timer finishes at 5s and the callback is queued — but the call stack is busy until 10s, so the event loop can't push it in until then. setTimeout guarantees a minimum delay, never an exact one.AI-generated figure

This is JavaScript's concurrency model, and it's the source of setTimeout's "trust issues." The first rule follows directly: never block the main thread — there's only one call stack, so slow synchronous work freezes everything. And setTimeout(fn, 0)? Even with a 0ms timer the callback still takes the long way round — Web API → callback queue → wait for an empty stack — so it's a clean trick to defer low-priority work just past the current code.

timeout-zero.jsJavaScript
console.log("Start");
setTimeout(() => console.log("Callback"), 0);
console.log("End");

// Start, End, Callback
// 0ms still means "after the current synchronous code finishes".

Inside the JS engine (and V8)

JavaScript runs everywhere — browsers, servers, even watches — because of the JavaScript Runtime Environment (JRE): a container holding the JS engine plus the Web APIs, the event loop, and the queues. The engine is its heart. ECMAScript is the standard every engine follows — V8 (Chrome, Node), SpiderMonkey (Firefox), Chakra (old IE).

The engine isn't special hardware — it's a program (written in C++) that takes your high-level JS and runs it in three steps: parse, compile, and execute.

JS Engine1 · Parsecode → tokens→ AST2 · CompileJITinterpret + optimize3 · ExecuteMemory Heap+ Call StackJIT: interpret now, optimize hot paths as it runs
Fig 12Parse turns source into tokens and an AST; compile runs JIT — an interpreter starts immediately while a compiler optimizes hot code; execute runs it on the call stack against the memory heap, with a garbage collector cleaning up.AI-generated figure

V8, Google's engine, names its pieces: Ignition (the interpreter), TurboFan (the optimizing compiler), and Orinoco (the garbage collector). Different vendors, same job — turn your JS into fast machine code.

V8, in kid-sized words

Forget the jargon for a second. Picture V8 as a tiny kitchen that turns your recipe (the code) into a finished meal (a running program). You hand it the recipe; it reads it, starts cooking right away, and quietly gets faster and tidier as it goes.

That's the whole engine in one breath: read it, cook it fast, make the popular dishes even faster, and keep it clean.

Now the real thing. V8 is a tiered engine: it always starts interpreting immediately, then promotes code to faster and faster machine code the more it runs. Your source flows through one pipeline:

PARSINGIGNITIONINTERPRETERBYTE CODEEXECUTIONTURBOFANCOMPILEROPTIMIZEDMACHINE CODE OptimizationDeoptimization
Fig 13V8's compile-and-execute cycle: source is parsed, Ignition interprets it to bytecode, and execution runs. Hot code is handed to the TurboFan compiler (optimization) and run as optimized machine code; when an assumption breaks it deoptimizes back to the interpreter. Both bytecode and optimized machine code feed execution.AI-generated figure

One more trick behind the speed: hidden classes (V8 calls them Maps, or Shapes elsewhere). Objects created the same way share one hidden class, so V8 can reach a property by fixed offset instead of a dictionary lookup — and inline caches remember where that property lived last time. The practical rule: give objects their properties in the same order and don't bolt on new ones later, or you spawn new hidden classes and lose the fast path.

Higher-order functions and functional programming

A higher-order function (HOF) is simply a function that takes another function as an argument and/or returns one. The function passed in is the callback — you've been using HOFs all along (setTimeout, addEventListener, map).

hof.jsJavaScript
function greet() { console.log("Hi"); }

function caller(fn) {
  fn();          // caller is the higher-order function
}
caller(greet);   // "Hi"   — greet is the callback

Why it matters: it keeps you DRY. Say you need the area of each radius — easy. Then you also need the circumference, and you copy the whole loop with one line changed. Two near-identical functions is a smell. Instead, extract the logic into its own function and pass it into a single reusable loop.

dry.jsJavaScript
const radii = [1, 2, 3, 4];

const area = (r) => Math.PI * r * r;
const circumference = (r) => 2 * Math.PI * r;

// One loop, any operation — calculate() is the higher-order function:
const calculate = function (arr, operation) {
  const output = [];
  for (let i = 0; i < arr.length; i++) {
    output.push(operation(arr[i]));
  }
  return output;
};

calculate(radii, area);          // [3.14, 12.57, 28.27, 50.27]
calculate(radii, circumference); // [6.28, 12.57, 18.85, 25.13]
one loop, swappable logicareacircumferencecalculate()the reused loopoutput[ ]calculate() is a polyfill of map()
Fig 14The loop never changes; only the operation plugged into it does. Extracting the changing logic into a function is the heart of functional programming — and calculate(arr, op) is essentially a polyfill of map.AI-generated figure

That calculate is exactly what map does — radii.map(area) is the same idea. You can even bolt it onto Array.prototype so it's called like a built-in method:

polyfill.jsJavaScript
Array.prototype.calculate = function (operation) {
  const output = [];
  for (let i = 0; i < this.length; i++) {
    output.push(operation(this[i]));
  }
  return output;
};

radii.calculate(area); // [3.14, 12.57, 28.27, 50.27] — works like map

map, filter, reduce

Three higher-order array methods that replace most hand-written loops. map transforms every element into a new array of the same length; filter keeps only the elements that pass a test; reduce collapses the whole array down to a single value.

[5,1,3,2,6]map ×2[10,2,6,4,12][5,1,3,2,6]filter odd[5,1,3][5,1,3,2,6]reduce +17
Fig 15Same input array, three jobs: map transforms each element, filter selects a subset, and reduce folds everything into one value.AI-generated figure
map-filter.jsJavaScript
const arr = [5, 1, 3, 2, 6];

// map: transform each element
const doubled = arr.map((x) => x * 2);   // [10, 2, 6, 4, 12]

// filter: keep the ones that pass the test (odd numbers here)
const odds = arr.filter((x) => x % 2);   // [5, 1, 3]
reduce.jsJavaScript
const arr = [5, 1, 3, 2, 6];

// reduce: fold the array into one value.
// (accumulator, current) plus a starting value as the 2nd argument.
const sum = arr.reduce((acc, cur) => acc + cur, 0);  // 17
const max = arr.reduce((acc, cur) => (cur > acc ? cur : acc), 0); // 6

Because map and filter each return a new array, you can chain them — read it left to right like a sentence.

chaining.jsJavaScript
const users = [
  { firstName: "Alok",   age: 23 },
  { firstName: "Ashish", age: 29 },
  { firstName: "Pranav", age: 50 },
];

// "Names of everyone under 30":
const names = users
  .filter((u) => u.age < 30)
  .map((u) => u.firstName);   // ["Alok", "Ashish"]

The dark side of callbacks: callback hell

Callbacks are essential for async work, but lean on them and two problems appear — callback hell and inversion of control. Both are the reason promises exist.

Picture an e-commerce checkout: create the order, then proceed to payment (which needs the order), then show a summary, then update the wallet — each step depends on the one before. With callbacks you nest each step inside the previous one's callback, and the code drifts rightward:

callback-hell.jsJavaScript
createOrder(cart, function (orderId) {
  proceedToPayment(orderId, function (paymentInfo) {
    showOrderSummary(paymentInfo, function (balance) {
      updateWallet(balance);
    });
  });
});
// each step nested in the last — the code grows sideways
Pyramid of DoomcreateOrder(() => {proceedToPayment(() => {showOrderSummary(() => {updateWallet()
Fig 16Each dependent step nests inside the previous callback, so the code drifts right into a triangle — the Pyramid of Doom. Deep nesting like this is hard to read and harder to maintain.AI-generated figure

The subtler problem is inversion of control. When you pass proceedToPayment as a callback to createOrder, you hand a critical piece of your code to another function and trust it to call your callback — once, at the right time. What if it never calls it, calls it twice, or was written by someone else and breaks? You've lost control of your own flow.

Promises

A promise is a placeholder for a future value — an object representing the eventual completion (or failure) of an async operation. Instead of passing a callback into createOrder, you have it return a promise, and you attach your callback to that promise with .then.

promise-consume.jsJavaScript
const promise = createOrder(cart); // returns a promise immediately

// .then's callback fires automatically once the data is ready:
promise.then(function (orderId) {
  proceedToPayment(orderId);
});

A real promise object carries a state (pendingfulfilled or rejected) and a result (undefined until it settles). fetch returns one: it fires off the request and hands you a pending promise instantly, without blocking.

a Promisependingstate: pendingresult: undefinedresolve(data)fulfilledstate: fulfilledresult: data.then(cb) firesor reject → .catch
Fig 17A promise starts pending with no result. resolve(data) moves it to fulfilled and fires the attached .then; a reject sends it to .catch instead. It settles exactly once.AI-generated figure

Why this beats callbacks: you attach a callback instead of passing one. The promise guarantees it calls your callback once, and only once, when the data is ready — so inversion of control is gone. Promises are also immutable: once settled, the value can't be tampered with.

fetch.jsJavaScript
const user = fetch("https://api.github.com/users/alok722");
console.log(user); // Promise { <pending> }

user.then(function (data) {
  console.log(data); // runs once the response is ready
});

Creating promises, chaining, and errors

On the producer side, you build a promise with new Promise((resolve, reject) => …). JS hands you resolve and reject — call resolve(value) on success, reject(error) on failure.

promise-create.jsJavaScript
function createOrder(cart) {
  return new Promise(function (resolve, reject) {
    if (!validateCart(cart)) {
      reject(new Error("Cart is not valid")); // failure
      return;
    }
    const orderId = "12345";   // e.g. from the database
    resolve(orderId);          // success
  });
}

On the consumer side, .then handles success and .catch handles failure. A rejected promise (or a thrown error) skips ahead to the nearest .catch.

Promise chainingcreateOrder(cart).then → proceedToPayment.then → showOrderSummary.catch(err)any reject lands here
Fig 18Return a promise from each .then and its result becomes the next .then's input — the pyramid flattens into a vertical chain. A single .catch at the end handles a rejection anywhere above it.AI-generated figure

That return is the whole trick behind promise chaining: whatever you return from one .then becomes the input to the next. Forgetting to return is the classic pitfall — the chain breaks. One .catch at the end catches a rejection anywhere above it; put a .catch mid-chain if you want the rest to run even after a failure.

promise-chain.jsJavaScript
createOrder(cart)
  .then(function (orderId) {
    return proceedToPayment(orderId);   // return → next .then's input
  })
  .then(function (paymentInfo) {
    return showOrderSummary(paymentInfo);
  })
  .then(function (balance) {
    return updateWallet(balance);
  })
  .catch(function (err) {
    console.log(err);   // any rejection above lands here
  });

async / await

async/await is syntactic sugar over promises — the same machinery, written so it reads top to bottom like synchronous code. An async function always returns a promise: return a plain value and it's wrapped in a resolved promise; return a promise and it's handed back as-is.

async-returns.jsJavaScript
async function getData() {
  return "Namaste JavaScript";
}
getData();                       // Promise { <fulfilled>: 'Namaste JavaScript' }
getData().then((v) => console.log(v)); // "Namaste JavaScript"

await — usable only inside an async function — pauses that function until the promise settles, then hands back the resolved value directly, no .then needed. Using await outside an async function is a SyntaxError.

await.jsJavaScript
const p = new Promise((resolve) =>
  setTimeout(() => resolve("done!"), 3000),
);

async function handle() {
  const val = await p;   // function pauses here until p settles
  console.log(val);      // "done!" — after 3s, no .then needed
}
handle();

The key behaviour: at await the function looks like it's waiting, but JS is never blocked. The function is suspended and taken off the call stack, so the stack stays free and the page never freezes; when the promise resolves, the function is pushed back on and resumes from exactly where it paused.

await suspends, never blockshandlePromiselogs "Hi"awaitsuspendedoff the stackresolvedresumesthe call stack stays free the whole time
Fig 19At await, handlePromise is suspended and removed from the call stack — nothing blocks. Once the promise resolves, the function is put back on the stack and continues from the line it paused on.AI-generated figure

One gotcha worth knowing: promises start running when they're created, not when they're awaited. Two promises created up front both start their timers immediately, so awaiting them one after another waits for the slower one — not the sum. To truly run them back-to-back, create each at its await.

In the real world you mostly await fetch, then await .json() (both return promises), and you swap .catch for a try/catch block:

async-fetch.jsJavaScript
async function getUser() {
  try {
    const res = await fetch("https://api.github.com/users/alok722");
    const data = await res.json();   // res.json() is a promise too
    console.log(data);
  } catch (err) {
    console.log(err);                // any await above that rejects lands here
  }
}
getUser();

Promise combinators: all, allSettled, race, any

Four static helpers handle several promises at once — think parallel API calls. They differ in when they settle and how they treat failure. For each below, assume three calls where p1 takes 3s, p2 takes 1s, and p3 takes 2s.

Promise combinators.allall fulfill → array · 1st reject fails fast.allSettledwaits for all · never rejects.racefirst to settle — resolve or reject.anyfirst to fulfill · all reject → AggregateError
Fig 20The four combinators at a glance: all needs every promise (and fails fast), allSettled always waits and never rejects, race takes the first to settle either way, and any takes the first success.AI-generated figure

Promise.all([p1, p2, p3]) waits for all to fulfill and returns their results in order — taking 3s, the slowest. But it fails fast: if any promise rejects, it rejects immediately with that error (the others keep running, but their results are discarded).

all.jsJavaScript
Promise.all([p1, p2, p3])
  .then((results) => console.log(results)) // ['P1','P2','P3'] after 3s
  .catch((err) => console.error(err));      // if p2 rejects at 1s → rejects at 1s

// All fulfilled → array of values, in input order.
// Any rejection  → Promise.all rejects right away with that first error.

Promise.allSettled([p1, p2, p3]) is the safe one: it waits for every promise to settle (3s) and never rejects. You get an array describing each outcome — { status, value } or { status, reason }.

all-settled.jsJavaScript
Promise.allSettled([p1, p2, p3]).then((results) => console.log(results));
// [
//   { status: 'fulfilled', value: 'P1 Success' },
//   { status: 'fulfilled', value: 'P2 Success' },
//   { status: 'rejected',  reason: 'P3 Fail' },
// ]

Promise.race([p1, p2, p3]) settles the moment the first promise settles — win or lose. With p2 fastest at 1s you get its value, but if the first to finish had rejected, race rejects. Promise.any([p1, p2, p3]) is similar but ignores rejections: it settles on the first fulfillment, and only if all reject does it reject — with an AggregateError (the individual errors live on err.errors).

race-any.jsJavaScript
// race → first to SETTLE (resolve or reject):
Promise.race([p1, p2, p3]).then(console.log).catch(console.error); // p2 at 1s

// any → first to FULFILL; all rejecting gives an AggregateError:
Promise.any([p1, p2, p3])
  .then(console.log)
  .catch((err) => console.error(err.errors)); // ['P1 Fail','P2 Fail','P3 Fail']

The this keyword

this refers to an object — and which object depends entirely on how the function is called, not where it's written. Here's the whole rundown.

what this refers toglobalthe global object (window)functionundefined · window if sloppyobj.method()the object before the dotarrow fninherits enclosing thisDOM handlerthe HTML element
Fig 21this is set at call time, not definition time: the global object at the top level, the object before the dot for a method, whatever the enclosing scope holds for an arrow, and the element for a DOM handler.AI-generated figure

In the global space, this is the global object (window in a browser; it differs by runtime). Inside a regular function it's undefined in strict mode. In non-strict ("sloppy") mode, this substitution swaps that undefined for the global object — which is why a plain call reads as window there.

this-function.jsJavaScript
function x() {
  console.log(this);
  // strict mode      → undefined
  // non-strict mode  → window  (this substitution)
}
x();        // depends on mode
window.x(); // window — because of how it's called

Inside an object method, this is the object the method was called on — the part before the dot. And call, apply, and bind let you set this explicitly, so you can borrow a method for another object.

this-method.jsJavaScript
const student = {
  name: "Alok",
  printName: function () {
    console.log(this.name);
  },
};
student.printName();              // "Alok"  — this is student

// Borrow the method, set this explicitly:
const student2 = { name: "Kajal" };
student.printName.call(student2); // "Kajal" — this is student2

Arrow functions are the exception: they have no own this. They take it from the enclosing lexical scope. So an arrow used as an object method points at the outer this (often window), but an arrow inside a regular method inherits that method's this.

this-arrow.jsJavaScript
const obj = {
  a: 10,
  x: () => console.log(this), // window — arrow takes the enclosing this
  y: function () {
    const inner = () => console.log(this); // obj — inherits y's this
    inner();
  },
};
obj.x();
obj.y();

And in a DOM event handler, this is the element that fired the event — e.g. <button onclick="alert(this)"> alerts the button element.

One line to carry it all: JS sets up memory first (hoisting), runs the code line by line inside nested execution contexts, resolves names by walking the scope chain outward, runs async work through the event loop, and lets functions carry that scope with them — as closures, callbacks, and the promises (and async/await) that finally tame async.

All issues