MAD Study Hub

Home / MAD2 / Week 2

Week 2 · Advanced JavaScript: closures, promises and async/await

Official syllabus line: Advanced JavaScript
About 110 minQuiz 1 material (see exam radar)BSCS2006 (+ project BSCS2006P) · Thejesh G N, Nitin Chandrachoodan
Before you start: MAD2 week 1 (functions, arrays, objects, JSON). Know that a callback is a function you pass to another function.

The one-minute story

Think of it like this

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 this for method, arrow, call and bind;
  • write classes with extends and super, and explain prototypes;
  • use try/catch/finally, throw, ?. and ??;
  • predict the printing order of code mixing setTimeout, promises and await;
  • write fetch code with async/await, response.ok and a JSON POST.

Words you need

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 export and import.
  • Call stack - the pile of functions currently running.
  • Task queue - where finished timers and events wait (setTimeout callbacks).
  • 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 then chains.
  • Fetch API - the built-in way to make HTTP requests; it returns a promise.
  • Optional chaining ?. - read a property but stop with undefined if something on the way is null/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 ]
The idea

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.

Your turn: 1. Closure
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 calledthis 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 functionthe this of the code around where it was written; call/bind cannot change it
new F() or classthe 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
Solved example: 1. A real this question
var 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.)

Watch out - real exam trap
  • An arrow function ignores call, apply and bind for this.
  • Extracting a method (const f = obj.method; f()) loses this.
  • A top-level var becomes a property of the global object in a browser script or REPL; let/const do not.
  • Arrow functions as object methods usually give the wrong this; use shorthand methods hi() {}.

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
  • constructor runs on new; super(...) must be called before this in a child constructor.
  • Methods live on the prototype, so Object.keys(d) shows only the data properties.
  • static methods belong to the class, not to objects.
Your turn: 2. Prototype lookup
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:

  1. Call stack: your running code. Always finishes completely first.
  2. Microtask queue: promise callbacks (then, catch, finally, code after await) and queueMicrotask.
  3. Task queue: timers (setTimeout), events, network callbacks.
The idea

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.

Your turn: 3. Order
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 then returns becomes the next input. Return a promise and the chain waits for it.
  • A throw (or a rejection) skips all following thens until a catch.
  • A catch that returns normally recovers: the chain continues as fulfilled.
  • finally always runs, but it need not be last, and a catch can 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
Solved example: 2. Timing of a chain
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' ]
Watch out - real exam trap
  • 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, calling resolve("x") actually rejects and runs the catch. (A real question: data.find(...) found the element, the code called the function named resolve, and the output was "Test Passed !! Element Found" from the catch.)
  • promise.then(cb) does not wait: code after it runs first.
  • Promise.all keeps 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
Your turn: 4. What is a?
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();
}
The idea

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

Solved example: 3. Exam: which message is logged?
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

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

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 throw in a then skipping to catch; a rejected third promise).
  • async/await ordering: "Statement 3" prints before "Statement 1" because of the first await; try/catch inside async functions.
  • this with call and 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; finally always runs; a catch need not follow a then.
Watch out - real exam trap
  • Sync code first, then microtasks (promises, after await), then timers.
  • An async function runs synchronously until its first await.
  • After a throw, only a catch (or a second argument to then) runs; the other thens are skipped.
  • fetch does not reject on HTTP errors.
  • Each then returning a new timed promise means the delays add up.

Quick quiz

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
Sync lines A and D first, then the microtask C, then the timer task 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
Each 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.
There are three states: pending is the starting one.

Select all TRUE statements. (select all that apply)

  • A finally callback runs whether the promise was fulfilled or rejected.
  • A catch can be attached directly to a promise without a then before it.
  • A finally must always be the last call in a chain.
  • fetch rejects 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
  • this is decided by how the function is called, and there is no object before the dot
  • const freezes this
  • arrow functions always lose this
The call site decides 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
A child constructor must call super() before using this.

Viva: say it out loud

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

Remember this
  • 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 gives undefined (strict) or global, call/apply/bind set it, arrows borrow it.
  • Objects look up missing properties along the prototype chain; class and extends build that chain; super() comes first.
  • Modules use export and import (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; then returns a new promise; throw skips to catch; finally always runs.
  • async functions return promises; await pauses only that function; sequential waits add up.
  • fetch: check response.ok; res.json() is a promise; a POST needs method, header and a stringified body.
Code and facts to keep
  • 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, after await), 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();
Connected weeks (learn them together)

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