Week 2 · Advanced JavaScript: closures, promises and async/await
The one-minute story
Ordering coffee at a busy cafe. You tell the cashier your order and get a token with a number. You do not stand frozen at the counter; you sit down, read your phone, and when the barista calls your number you collect the coffee. JavaScript works like this. It has only one cashier (it does one thing at a time), so for slow jobs such as a network request it hands a note to the browser, carries on, and later is called back when the answer is ready.
A Promise is the token: "your result will come, or an error will". async/await is a way to write that waiting code so it reads top to bottom. A closure is a function that carries a small backpack with the variables it was born next to. And the cashier has a strict queue rule (the event loop) that decides who gets served first: this week you learn the rule well enough to predict the printing order in exam questions.
By the end of this week you can:
- explain closures and the scope chain, and predict
thisfor method, arrow,callandbind; - write classes with
extendsandsuper, and explain prototypes; - use
try/catch/finally,throw,?.and??; - predict the printing order of code mixing
setTimeout, promises andawait; - write
fetchcode withasync/await,response.okand a JSON POST.
Words you need
- Closure - a function together with the variables from the place where it was created.
- Scope chain - the look-up path: own scope, then the enclosing scope, and so on up to global.
- Higher-order function - a function that takes a function or returns a function (
map,filter). - Prototype - the object an object falls back to when a property is not found on itself.
- Class - a template for making objects; it is neat syntax over prototypes.
- Module - a JS file with its own scope that shares things with
exportandimport. - Call stack - the pile of functions currently running.
- Task queue - where finished timers and events wait (
setTimeoutcallbacks). - Microtask queue - a faster queue for promise callbacks; it is emptied before the next task.
- Event loop - the loop that moves waiting work onto the call stack when the stack is empty.
- Promise - an object for a future result: pending, fulfilled or rejected.
- async/await - syntax that lets you wait for a promise inside a function without
thenchains. - Fetch API - the built-in way to make HTTP requests; it returns a promise.
- Optional chaining
?.- read a property but stop withundefinedif something on the way isnull/undefined.
Idea 1: Scope chain and closures
When JavaScript meets a name, it looks in the current scope, then the one outside it, and so on to the global scope. This is the scope chain, and it is fixed by where the code is written (lexical scope), not where it is called.
A closure is what you get when an inner function remembers the variables of its outer function even after the outer function has finished.
function makeCounter() {
let count = 0; // private: nobody outside can touch it
return function () { count++; return count; };
}
const c1 = makeCounter(), c2 = makeCounter();
console.log(c1(), c1(), c1()); // 1 2 3
console.log(c2()); // 1 c2 has its own separate backpack
console.log(typeof count); // undefined (not visible outside)
makeCounter has finished, yet count lives on because the returned function still uses it. Each call to makeCounter creates a new count.
Closures also explain the loop puzzle from week 1:
const fs = [];
for (var i = 0; i < 3; i++) fs.push(() => i); // all three share ONE i
console.log(fs.map(f => f())); // [ 3, 3, 3 ]
const gs = [];
for (let j = 0; j < 3; j++) gs.push(() => j); // a new j per round
console.log(gs.map(g => g())); // [ 0, 1, 2 ]
Callback: a function passed to another to be called later. Higher-order function: a function that takes or returns functions. arr.map(fn), setTimeout(fn, ms) and addEventListener("click", fn) all take callbacks, and the callbacks usually use closures to reach the variables around them.
function bank() { let bal = 100; return { add: n => (bal += n), get: () => bal }; }
const a = bank(); a.add(50); const b = bank();
console.log(a.get(), b.get());Need a hint?
Each bank() call creates its own bal.
Show the full solution
150 100. a and b have separate bal variables; only add and get can reach them (this is how you make "private" data).
Idea 2: this rules
this is decided at call time (except in arrow functions). Learn the five cases:
| How the function is called | this is |
|---|---|
obj.method() | obj (the object before the dot) |
plain f() | undefined in strict mode and modules; the global object (window) in sloppy browser scripts |
f.call(x, a, b) / f.apply(x, [a, b]) | x (runs now) |
const g = f.bind(x) | x forever (returns a new function) |
| arrow function | the this of the code around where it was written; call/bind cannot change it |
new F() or class | the new object |
"use strict";
const user = {
name: "Asha",
hi() { return "Hi " + this.name; },
later() { return [1].map(() => this.name); }, // arrow keeps this = user
};
console.log(user.hi(), user.later()); // Hi Asha [ 'Asha' ]
const lost = user.hi;
try { lost(); } catch (e) { console.log("TypeError: lost this"); }
const bound = user.hi.bind({ name: "Ravi" });
console.log(bound(), user.hi.call({ name: "Meena" })); // Hi Ravi Hi Meena
function add(a, b) { return this.base + a + b; }
console.log(add.call({ base: 100 }, 1, 2), add.apply({ base: 100 }, [1, 2])); // 103 103
this questionvar first = 30;
const obj1 = { first: 50, second: () => { console.log("Value:", this.first); } };
const obj2 = { first: 80, second: function () { console.log("Value:", this.first); this.second(); } };
obj2.second.call(obj1);
Run in a browser console or REPL (not as a Node module file). Step 1: call(obj1) runs obj2.second with this = obj1, so it prints Value: 50. Step 2: inside, this.second() is obj1.second, which is an arrow function. Its this is the outside this, the global object, where var first = 30 created a global property first. It prints Value: 30. Answer: 50 then 30. (Inside a Node file var first stays module-private and this is {}, so the second line would print undefined: that is why the question says REPL.)
- An arrow function ignores
call,applyandbindforthis. - Extracting a method (
const f = obj.method; f()) losesthis. - A top-level
varbecomes a property of the global object in a browser script or REPL;let/constdo not. - Arrow functions as object methods usually give the wrong
this; use shorthand methodshi() {}.
Idea 3: Prototypes and classes
Every object has a hidden link (its prototype). If a property is not found on the object, JavaScript follows the link, and so on up the chain; if it reaches the end it gives undefined.
const Obj1 = { model: "AIR101", brand: "AIRBUS", type: "Passenger" };
const Obj2 = { __proto__: Obj1, wheels: "36", engines: 4 };
console.log(Obj2.model); // AIR101 found on the prototype
console.log(Object.keys(Obj2).length); // 2 own keys only: wheels, engines
console.log(Obj1.engines); // undefined the link only goes UP, never down
console.log(Obj2.hasOwnProperty("model"), "model" in Obj2); // false true
A class is tidy syntax for the same machinery:
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} makes a sound`; }
static kind() { return "animal"; }
}
class Dog extends Animal {
constructor(name, breed) { super(name); this.breed = breed; } // super() FIRST
speak() { return super.speak() + " (woof)"; }
}
const d = new Dog("Tommy", "lab");
console.log(d.speak()); // Tommy makes a sound (woof)
console.log(d instanceof Dog, d instanceof Animal, Animal.kind()); // true true animal
console.log(Object.keys(d), typeof Animal); // [ 'name', 'breed' ] function
constructorruns onnew;super(...)must be called beforethisin a child constructor.- Methods live on the prototype, so
Object.keys(d)shows only the data properties. staticmethods belong to the class, not to objects.
const a = { x: 1 }; const b = { __proto__: a, y: 2 }; const c = { __proto__: b, z: 3 };
console.log(c.x, c.y, c.w, Object.keys(c));Need a hint?
Follow the chain c, then b, then a.
Show the full solution
1 2 undefined [ 'z' ]. x is found on a two links up, w is nowhere, and Object.keys lists only own keys.
Idea 4: Modules, errors and safe access
ES modules split code into files. Each file has its own scope; you share with export and take with import.
// browser: math.js
export const PI = 3.14;
export function area(r) { return PI * r * r; }
export default function hello() { return "hi"; }
// app.js
import hello, { area, PI as pi } from "./math.js"; // default without braces, named with
In HTML: <script type="module" src="app.js"></script>. Modules are in strict mode, run after the page is parsed, and need the file path with extension (./router.js). Browsers refuse module files opened from file://, so use a small server (for example npx http-server).
Errors
function risky(n) {
try {
if (n < 0) throw new Error("negative");
return "ok";
} catch (e) {
return "caught " + e.message;
} finally {
console.log("finally runs"); // runs on success, on error and even after return
}
}
console.log(risky(1)); // finally runs / ok
console.log(risky(-1)); // finally runs / caught negative
try { null.x; } catch { console.log("TypeError caught, no variable needed"); }
try { undefinedName; } catch (e) { console.log(e.name); } // ReferenceError
throw can throw anything, but throw an Error. A try/catch only catches errors that happen while it runs: it cannot catch an error thrown later inside a setTimeout callback (use .catch or await for promises).
Optional chaining and nullish coalescing
const u = { profile: { city: "Pune" }, list: [] };
console.log(u.profile?.city, u.address?.city, u.address?.city ?? "unknown"); // Pune undefined unknown
console.log(u.list?.[0], u.run?.()); // undefined undefined
a?.b gives undefined instead of an error when a is null or undefined. x ?? y uses y only for null or undefined (not for 0 or "").
Idea 5: The event loop
Single thread, one call stack. Waiting work goes elsewhere (the browser timers, the network), and when ready its callback is queued:
- Call stack: your running code. Always finishes completely first.
- Microtask queue: promise callbacks (
then,catch,finally, code afterawait) andqueueMicrotask. - Task queue: timers (
setTimeout), events, network callbacks.
The rule: run the stack to empty. Then run all microtasks (including new ones they add). Then take one task from the task queue. Repeat. So promise callbacks run before any setTimeout, even setTimeout(fn, 0).
console.log("1 start");
setTimeout(() => console.log("2 timeout"), 0);
Promise.resolve().then(() => console.log("3 promise"));
queueMicrotask(() => console.log("4 microtask"));
console.log("5 end");
// 1 start, 5 end, 3 promise, 4 microtask, 2 timeout
Synchronous lines 1 and 5 first; then microtasks in the order they were queued (3, 4); then the timer task (2). A setTimeout(fn, 0) also does not mean "now": it means "after the stack and microtasks are done".
async function f() { console.log("a"); await null; console.log("b"); }
f(); console.log("c"); // a c b
An async function runs synchronously until its first await; the rest is a microtask afterwards.
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
(async () => { console.log("D"); await 0; console.log("E"); })();
console.log("F");Need a hint?
Sync lines first, then microtasks in order, then timers.
Show the full solution
A D F C E B. Sync: A, then D (the async function body runs until await), then F. Microtasks: C, then E. Task: B.
Idea 6: Promises
A promise is in one of three states: pending, fulfilled (has a value) or rejected (has a reason). Once settled it never changes.
const p = new Promise((resolve, reject) => { // first param RESOLVES, second REJECTS
setTimeout(() => resolve("done"), 100);
});
p.then(v => { console.log(v); return v + "!"; }) // then returns a NEW promise
.then(v => console.log(v)) // gets "done!"
.finally(() => console.log("cleanup")); // runs either way; passes result along
Prints done, done!, cleanup.
Rules of a chain (these are tested again and again):
- Whatever a
thenreturns becomes the next input. Return a promise and the chain waits for it. - A throw (or a rejection) skips all following
thens until acatch. - A
catchthat returns normally recovers: the chain continues as fulfilled. finallyalways runs, but it need not be last, and acatchcan be attached straight to a promise.
Promise.resolve(1)
.then(v => { console.log("a", v); throw new Error("boom"); })
.then(() => console.log("skipped"))
.catch(e => { console.log("caught", e.message); return "recovered"; })
.then(v => console.log("then", v));
// a 1, caught boom, then recovered
const p1 = new Promise(res => setTimeout(() => res("First"), 1000));
p1.then(r => { console.log(r); return new Promise(res => setTimeout(() => res("Second"), 3000)); })
.then(r => { console.log(r); return new Promise(res => setTimeout(() => res("Third"), 3000)); })
.then(r => console.log(r));
Each then returns a new timed promise, so they run one after another: First at 1 second, Second at 4, Third at 7. Total: 7 seconds (1 + 3 + 3), not 3. If the third promise called reject, the last then would be skipped and a catch would print the reason instead.
Combining promises
const wait = (ms, v, ok = true) => new Promise((res, rej) => setTimeout(() => (ok ? res(v) : rej(v)), ms));
Promise.all([wait(30, "a"), wait(10, "b")]).then(console.log); // [ 'a', 'b' ] in input order
Promise.all([wait(30, "a"), wait(10, "x", false)]).catch(console.log); // x (fails as soon as one fails)
Promise.race([wait(30, "slow"), wait(10, "fast")]).then(console.log); // fast (first to settle)
Promise.allSettled([wait(5, 1), wait(5, 2, false)]).then(r => console.log(r.map(x => x.status)));
// [ 'fulfilled', 'rejected' ]
- A promise has three states, not two. "Only fulfilled and rejected" is the false statement.
new Promise((reject, resolve) => ...): parameter names do not matter, order does. The first is always resolve. With swapped names, callingresolve("x")actually rejects and runs thecatch. (A real question:data.find(...)found the element, the code called the function namedresolve, and the output was "Test Passed !! Element Found" from thecatch.)promise.then(cb)does not wait: code after it runs first.Promise.allkeeps input order, not finishing order.
Idea 7: async/await
async before a function makes it always return a promise. await pauses that function (not the whole program) until a promise settles, then gives you its value or throws its error. Use try/catch for errors.
const wait = (ms, v) => new Promise(res => setTimeout(() => res(v), ms));
async function test() {
const x1 = await wait(1000, 1);
console.log(2); // (2, 1 second)
const x2 = await wait(3000, 3);
console.log(x1); // (1, 4 seconds)
console.log(x2); // (3, 4 seconds)
}
test();
console.log(4); // (4, 0 seconds)
Output with times: (4, 0) (2, 1) (1, 4) (3, 4). The 4 comes first because test() hands back control at the first await; the two waits are sequential, so 1 + 3 = 4 seconds. To run waits in parallel, start both first: const [a, b] = await Promise.all([wait(1000, 1), wait(3000, 3)]) takes only 3 seconds.
async function demo() {
try {
console.log(await Promise.resolve("Start"));
await new Promise((res, rej) => setTimeout(() => rej("Error"), 50));
console.log("never");
} catch (e) { console.log("Caught:", e); }
return "End";
}
demo().then(r => console.log(r)); // Start, Caught: Error, End
async function t() { const a = await new Promise(r => r(2 && 4)); console.log(a); }
t();Need a hint?
2 && 4 returns an operand.
Show the full solution
It prints 4: && returns the last operand when both are truthy, the promise resolves with 4, and await unwraps it.
Idea 8: The Fetch API, JSON and generators
fetch(url, options) returns a promise of a Response. response.json() returns another promise of the parsed body.
async function getJSON(url) {
const res = await fetch(url); // rejects ONLY on network failure
if (!res.ok) throw new Error("HTTP " + res.status); // 404/500 still resolve; check ok (200-299)
return res.json(); // rejects if the body is not valid JSON
}
async function postJSON(url, obj, token) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: "Bearer " + token },
body: JSON.stringify(obj), // body must be text
});
if (!res.ok) throw new Error(res.statusText || "HTTP " + res.status);
return res.json();
}
Fetch rules. (1) A 404 or 500 is not an error for fetch; check response.ok or response.status. (2) The body is read once, with .json(), .text(). (3) A POST needs method, a Content-Type header and a JSON.stringify-ed body. (4) If the server returns HTML but you call .json(), you get a parse error (a rejected promise).
async function FetchFunct(url) {
const response = await fetch(url).catch(() => { throw new Error("Network Error"); });
if (response.ok) {
return await response.json().catch(() => { throw new Error("Unexpected Error"); });
} else throw new Error(response.statusText);
}
FetchFunct("dummyUrl").then(console.log).catch(err => console.log(err.message));
If the URL answers 200 with an HTML page: the network works (no "Network Error"), ok is true, but .json() cannot parse HTML, so the inner catch throws "Unexpected Error". A 404 would give the status text ("Not Found"); a dead server would give "Network Error".
Generators and iterators (briefly). A function* can pause with yield. Calling it returns an iterator whose next() gives { value, done }. for...of and spread use this protocol.
function* count() { yield 1; yield 2; return 3; }
const it = count();
console.log(it.next(), it.next(), it.next(), it.next());
// { value: 1, done: false } { value: 2, done: false } { value: 3, done: true } { value: undefined, done: true }
console.log([...count()]); // [ 1, 2 ] spread ignores the return value
In your project, every fetch should have: await, a response.ok check, a try/catch, and a loading flag that disables the button while waiting (a real question asked exactly this: clicking a button quickly started overlapping requests, and the fix was to disable the button while loading). The examiner may ask "what happens if the server is down, and what if it returns 404?".
Exam radar
The keyword scan matches about 160 distinct graded questions for this week (about 138 of 848 in Quiz 1, 180 of 608 in Quiz 2, 263 of 1471 in the End Term; keyword based, so approximate). Many are Vue questions, but the JavaScript underneath is exactly this lesson. The questions that appear again and again are:
- Promise chains: what is printed, in what order, or how many seconds (the 1 + 3 + 3 = 7 second question; a
throwin athenskipping tocatch; a rejected third promise). - async/await ordering: "Statement 3" prints before "Statement 1" because of the first
await; try/catch inside async functions. thiswithcalland arrow functions.- Prototype lookup (
__proto__, own keys count). - Fetch error handling (which message when the body is HTML).
- True/false: a promise has three states;
finallyalways runs; acatchneed not follow athen.
- Sync code first, then microtasks (promises, after
await), then timers. - An async function runs synchronously until its first
await. - After a
throw, only acatch(or a second argument tothen) runs; the otherthens are skipped. fetchdoes not reject on HTTP errors.- Each
thenreturning a new timed promise means the delays add up.
Quick quiz
What is printed? console.log("A"); setTimeout(() => console.log("B"), 0); Promise.resolve().then(() => console.log("C")); console.log("D");
- A B C D
- A D B C
- A D C B
- A C D B
A chain of three thens returns promises that resolve after 1, 3 and 3 seconds. The minimum time for all to finish is...
- 3 seconds
- 1 second
- 7 seconds
- 0 seconds
then waits for the promise returned by the previous one, so the delays add up.Which statement is FALSE about promises?
- A promise has the states pending, fulfilled and rejected.
- Promises can be chained.
- A promise has only two states, fulfilled and rejected.
- The constructor takes an executor function with resolve and reject.
Select all TRUE statements. (select all that apply)
- A
finallycallback runs whether the promise was fulfilled or rejected. - A
catchcan be attached directly to a promise without athenbefore it. - A
finallymust always be the last call in a chain. fetchrejects its promise on a 404 response.
finally can be followed by more calls, and fetch only rejects on network failure.const f = obj.method; f() loses this because...
- methods cannot be copied
thisis decided by how the function is called, and there is no object before the dotconstfreezesthis- arrow functions always lose
this
this, not where the function was written (arrows are the exception).What does class B extends A { constructor() { this.x = 1; } } do when new B() runs?
- Creates the object with x = 1
- Returns undefined
- Throws a ReferenceError because super() was not called first
- Calls A's constructor automatically
super() before using this.Viva: say it out loud
What is a closure and where have you used one?
A closure is a function that keeps access to the variables of the place where it was created, even after that outer function has returned. Event handlers, setTimeout callbacks and factory functions like makeCounter use closures to remember data.
Explain the event loop in simple words.
JavaScript runs one thing at a time on the call stack. Slow work such as timers and network calls is done by the browser, and the finished callbacks wait in queues. When the stack is empty, the loop runs all promise callbacks (microtasks) first and then one timer or event callback (a task).
What is the difference between a promise and async/await?
They are the same thing written differently. A promise is the object for a future result. async/await is syntax that lets me write await promise instead of then chains, and I handle errors with try/catch.
How do you handle errors in fetch?
fetch only rejects when the network fails, so I check response.ok and throw an error for 4xx or 5xx. I wrap the call in try/catch, and I also handle a body that is not valid JSON.
What decides the value of this?
The way a function is called: the object before the dot for methods, the object given to call, apply or bind, and the new object for new. Arrow functions have no this of their own and use the one around them.
What is the difference between a class and a prototype?
A class is a cleaner syntax; underneath, objects link to a prototype object for shared methods. extends links the prototypes, and super calls the parent's constructor or method.
What do Promise.all and Promise.race do?
Promise.all waits for every promise and gives an array of results in order, but rejects as soon as one fails. Promise.race settles with whichever promise settles first.
Recap
- A closure keeps the variables where a function was born. Callbacks and higher-order functions rely on it.
this: dot call gives the object, plain call givesundefined(strict) or global,call/apply/bindset it, arrows borrow it.- Objects look up missing properties along the prototype chain;
classandextendsbuild that chain;super()comes first. - Modules use
exportandimport(type="module");try/catch/finally,throw;?.and??make access safe. - Event loop: stack, then all microtasks (promises), then one task (timer).
- Promises: pending, fulfilled, rejected;
thenreturns a new promise;throwskips tocatch;finallyalways runs. asyncfunctions return promises;awaitpauses only that function; sequential waits add up.fetch: checkresponse.ok;res.json()is a promise; a POST needs method, header and a stringified body.
const make = () => { let c = 0; return () => ++c; };f.call(obj, a),f.apply(obj, [a]),const g = f.bind(obj)class B extends A { constructor(x) { super(x); } }export const x = 1; export default fn; import fn, { x } from "./m.js";try { } catch (e) { } finally { };a?.b,a ?? b- Order: sync, microtasks (
then, afterawait), tasks (setTimeout) Promise.all([..]),Promise.race([..]),Promise.allSettled([..])const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(obj) }); if (!res.ok) throw new Error(res.status); const data = await res.json();
- MAD1 week 8 - The frontend: JavaScript, the DOM and fetch: fetch returns a promise: async/await in MAD2 week 2 explains how to use it properly.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All Quiz 1 papers