๐Ÿ“œ

JavaScript Advanced

A Complete 5-Chapter Course

Topics covered:
Modules ยท Generators & Iterators ยท The Event Loop
Design Patterns ยท Working with APIs

Exercises: 15 hands-on challenges with sample solutions
Format: A4 ยท Dark-theme code examples ยท Quick-reference tables

Table of Contents

  1. Modules โ€” import/export, and Bundlers Conceptually
  2. Generators and Iterators
  3. The Event Loop In Depth
  4. Design Patterns
  5. Working with APIs
Chapter 1 of 5

Modules โ€” import/export, and Bundlers Conceptually

Course 3 ยท Ch 1
Modules: import/export, and Bundlers Conceptually
Splitting code across multiple files properly, instead of one giant script โ€” and what build tools actually do with that split

Every example across all 17 previous chapters lived in a single block of code. Real projects split logic across many files โ€” a module is simply a single JS file that explicitly declares what it shares with other files (export) and what it borrows from them (import). This is the standard, native mechanism JavaScript now has for organizing larger codebases.

Named Exports

// math.js export function add(a, b) { return a + b; } export const PI = 3.14159;

export in front of a function or variable declaration marks it as available to other files. A single file can have any number of named exports โ€” there's no limit, and they can be functions, constants, classes (Intermediate Chapter 3), anything.

Importing Named Exports

// main.js import { add, PI } from './math.js'; console.log(add(2, 3)); // 5 console.log(PI); // 3.14159

import { add, PI } from './math.js' uses object-destructuring-like syntax (Intermediate Chapter 1) to pull in specific named exports โ€” only what's listed inside the braces becomes available in this file, by exactly those names.

Default Exports

// logger.js export default function log(message) { console.log(`[LOG] ${message}`); } // main.js import log from './logger.js'; // no braces โ€” and the name can be anything log("App started");

A file can have at most one export default โ€” used for a file's single "main" thing, conventionally one per file (one component, one class, one primary function). Importing a default export uses no braces, and the imported name doesn't need to match what it was called in the original file โ€” import customName from './logger.js' would work identically.

A file can mix named and default exports
export default function log(...) alongside separate export const LOG_LEVELS = [...] in the same file is entirely valid โ€” import log, { LOG_LEVELS } from './logger.js' imports both together in one statement.

The <script type="module"> Requirement

<script type="module" src="main.js"></script>

Browsers only allow import/export inside a script explicitly marked type="module" โ€” a plain <script src="main.js"> (Fundamentals Chapter 1) throws a syntax error the moment it hits an import statement. Module scripts also behave slightly differently: they're deferred automatically (run after the HTML is parsed) and run in strict mode by default.

ES modules require a real server, not file:// directly
Opening an HTML file with type="module" scripts directly from disk (file:///...) typically fails with a CORS error in most browsers โ€” modules need to be served over http:// or https://, even just from a simple local development server, to load correctly.

Why Bundlers Exist

Native import/export works directly in modern browsers โ€” no extra tooling needed for small projects. But real-world apps often have dozens or hundreds of module files, each requiring its own network request, plus a desire to support older browsers, minify code for smaller downloads, and use newer syntax that needs converting. A bundler (Webpack, Vite, esbuild, Rollup) solves this: it reads the entire web of import/export statements across every file and combines them into one (or a few) optimized output files.

// Conceptually, a bundler turns this: // main.js -> imports math.js, logger.js // math.js -> exports add, PI // logger.js -> exports default log // ...into ONE combined, minified file like: const n=(e,t)=>e+t,o=3.14159;function r(e){console.log(`[LOG] ${e}`)}r("App started");

The shortened variable names, the removal of whitespace, and merging everything into one file are all things a bundler does automatically โ€” none of it changes the actual behaviour, only the file size and number of network requests needed to load the app. This course doesn't configure a real bundler, but understanding why one exists is essential before reaching for React, Vue, or any modern framework's tooling.

SyntaxHow many per file?Import syntax
export function/const/classAny numberimport { name } from '...'
export default ...At most oneimport anyName from '...'

Coding Challenges

Challenge 1

Write the contents of a file shapes.js with two named exports: a function circleArea(radius) and a constant TAX_RATE = 0.2. Then write the contents of main.js that imports both and logs circleArea(5) and TAX_RATE.

๐Ÿ“„ View solution
Challenge 2

Write the contents of a file validator.js with a default export โ€” a function isValidEmail(email) doing a simple check for the presence of "@" and ".". Then write main.js importing it under the name checkEmail (a different name than it was defined with) and testing it with two example strings.

๐Ÿ“„ View solution
Challenge 3

Write the contents of a file stringUtils.js that has BOTH a default export (a function capitalize(str)) and a named export (a function reverse(str)). Write main.js that imports both together in a single import statement and tests each one.

๐Ÿ“„ View solution

Chapter 1 Quick Reference

  • export function/const/class name โ€” a named export; any number per file
  • export default ... โ€” the default export; at most one per file
  • import { name } from './file.js' โ€” imports named exports by exact name
  • import anyName from './file.js' โ€” imports the default export; name is up to you
  • import def, { named } from './file.js' โ€” imports both kinds together
  • <script type="module"> โ€” required in the browser for import/export to work at all
  • Module scripts need a real server โ€” file:// alone typically fails with a CORS error
  • Bundlers (Webpack, Vite, esbuild) combine and optimize many module files into fewer, smaller ones
  • Next chapter: generators and iterators
Chapter 2 of 5

Generators and Iterators

Course 3 ยท Ch 2
Generators and Iterators
Functions that can pause mid-execution and hand back control โ€” the mechanism quietly powering for...of itself

Fundamentals Chapter 6 used for...of on arrays without explaining how it actually works under the hood. This chapter reveals the mechanism: the iterator protocol โ€” and generator functions, a special syntax that makes writing a custom iterator dramatically simpler than doing it by hand.

The Iterator Protocol, By Hand

function makeRangeIterator(start, end) { let current = start; return { next() { if (current < end) { return { value: current++, done: false }; } return { value: undefined, done: true }; } }; } const it = makeRangeIterator(1, 4); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false } console.log(it.next()); // { value: 3, done: false } console.log(it.next()); // { value: undefined, done: true }

An iterator is just an object with a next() method, returning { value, done } every call โ€” Intermediate Chapter 2's closure pattern, used here to remember current between calls. for...of on an array calls exactly this kind of next() repeatedly behind the scenes, stopping once done becomes true.

Generator Functions โ€” The Same Thing, Far Less Code

function* range(start, end) { for (let i = start; i < end; i++) { yield i; } } const it = range(1, 4); console.log(it.next()); // { value: 1, done: false } console.log(it.next()); // { value: 2, done: false }

function* (an asterisk after function) declares a generator function. Calling it doesn't run the body immediately โ€” it returns an iterator automatically, with all the { value, done } bookkeeping handled for free. yield is the key new keyword: it pauses execution exactly where it appears, hands back a value, and resumes from that exact point the next time next() is called.

yield is genuinely a pause, not just a return
Unlike return, which ends a function permanently, yield freezes the function's local state โ€” including the loop variable i above โ€” and resumes from that exact line on the next next() call. This is the entire reason generators can produce values one at a time, on demand, rather than computing everything up front.

Using a Generator Directly with for...of

function* range(start, end) { for (let i = start; i < end; i++) { yield i; } } for (const n of range(1, 4)) { console.log(n); } // 1 2 3

Because a generator's return value already follows the iterator protocol, for...of works on it directly โ€” no manual .next() calls needed at all. This is the practical payoff: write a generator once, then loop over it exactly like an array.

An Infinite Generator โ€” Why "Lazy" Evaluation Matters

function* infiniteCounter() { let n = 1; while (true) { yield n++; } } const counter = infiniteCounter(); console.log(counter.next().value); // 1 console.log(counter.next().value); // 2 console.log(counter.next().value); // 3 // Could keep calling .next() forever โ€” values are produced ON DEMAND, never all at once

infiniteCounter() contains a genuinely infinite while (true) loop, yet calling it doesn't hang the program โ€” nothing inside the loop body actually runs until .next() is explicitly called. This "lazy" behaviour (computing only what's actually requested) is impossible with a regular array, which would need to exist completely in memory before any of it could be used.

Never use for...of on an infinite generator directly
for (const n of infiniteCounter()) would loop forever, since for...of keeps calling .next() until done becomes true โ€” which never happens here. Infinite generators must be driven manually with .next(), typically combined with a separate stopping condition.

ConceptDescription
IteratorAny object with a next() method returning { value, done }
function* name() { }A generator function; calling it returns an iterator automatically
yield valuePauses the generator, hands back a value, remembers where it left off
for (const x of generatorCall())Works directly โ€” generators already satisfy the iterator protocol
Lazy evaluationValues are computed only when actually requested via next()

Coding Challenges

Challenge 1

Write a generator function* evens(max) that yields every even number from 0 up to max (inclusive). Use for...of to print every value it produces for evens(10).

๐Ÿ“„ View solution
Challenge 2

Write a generator function* fibonacci() that yields an infinite sequence of Fibonacci numbers (1, 1, 2, 3, 5, 8, ...), starting from two seed values. Manually call .next().value six times on an instance and log each result, WITHOUT using for...of (since it's infinite).

๐Ÿ“„ View solution
Challenge 3

Write a generator function* idGenerator(prefix) that yields strings like "prefix-1", "prefix-2", "prefix-3", incrementing forever. Create two SEPARATE generators with different prefixes ("user" and "order") and call .next().value three times on each, interleaved, to confirm they don't interfere with each other (similar in spirit to Intermediate Chapter 2's closure challenge).

๐Ÿ“„ View solution

Chapter 2 Quick Reference

  • Iterator โ€” any object with next(), returning { value, done }
  • function* name() { } โ€” generator function syntax; the asterisk is required
  • yield value โ€” pauses, returns a value, remembers exactly where execution stopped
  • Calling a generator returns an iterator immediately; the body doesn't run until .next() is called
  • for...of works directly on a generator's return value โ€” no manual .next() needed for finite generators
  • Infinite generators are safe โ€” nothing runs until .next() actually requests a value
  • Never for...of an infinite generator โ€” drive it manually with .next() instead
  • Next chapter: the event loop in depth โ€” microtasks vs macrotasks
Chapter 3 of 5

The Event Loop In Depth

Course 3 ยท Ch 3
The Event Loop In Depth: Microtasks vs Macrotasks
Why a promise's .then() always runs before a setTimeout, even one scheduled for 0 milliseconds

Fundamentals Chapter 10 explained that JavaScript doesn't wait for asynchronous work โ€” console.log lines after a setTimeout run before its callback. What that chapter didn't cover: when multiple asynchronous things are pending at once, JavaScript runs them in a very specific, predictable order โ€” governed by two separate queues, not one.

Recap: The Call Stack Runs Synchronous Code First

console.log("1"); setTimeout(() => console.log("2"), 0); console.log("3"); // 1, 3, 2 โ€” NOT 1, 2, 3, even with a 0ms delay

All ordinary, synchronous code finishes completely before the engine even considers running any scheduled callback โ€” this is the call stack emptying out first. setTimeout(..., 0) doesn't mean "run immediately"; it means "run as soon as the call stack is empty and it's this callback's turn," which is always after every line of synchronous code that follows it.

Two Queues, Not One

Once the call stack is empty, JavaScript doesn't pick from a single waiting line โ€” there are two: the microtask queue (promise callbacks, async function continuations) and the macrotask queue (setTimeout, setInterval, UI events). The rule: the entire microtask queue is fully drained before even one macrotask runs.

console.log("1: sync"); setTimeout(() => console.log("2: macrotask (setTimeout)"), 0); Promise.resolve().then(() => console.log("3: microtask (promise)")); console.log("4: sync"); // 1: sync // 4: sync // 3: microtask (promise) // 2: macrotask (setTimeout)

Even though setTimeout was scheduled before the promise's .then(), the microtask (the promise callback) still runs first โ€” because ALL synchronous code runs first, THEN every pending microtask, and only THEN the next macrotask. This ordering is fixed and predictable, not a coincidence of timing.

await is built on exactly this mechanism
Every await inside an async function (Fundamentals Chapter 10) schedules the rest of that function as a microtask โ€” which is precisely why an async function's code after await consistently runs before any pending setTimeout, no matter how the code is arranged on the page.

async/await Through the Same Lens

async function run() { console.log("A: start of run()"); await null; // resolves instantly, but STILL yields to the microtask queue console.log("C: after await"); } console.log("start"); run(); console.log("B: end of script"); // start // A: start of run() // B: end of script // C: after await

Calling run() executes synchronously up to the first await โ€” that part runs immediately, no different from a normal function. The moment await is hit, everything after it becomes a microtask, even though await null resolves essentially instantly. This is why "B" logs before "C", despite run() being called before "B"'s own console.log.

Why This Matters in Practice

Most real bugs from this come from assuming code "after" an async operation in the source file also runs "after" it in time. A common mistake: updating a UI element inside a .then(), then immediately reading that same element's state on the very next line โ€” that read happens before the microtask has had a chance to run, so it sees stale data. Understanding the queue order explains exactly why.

A long-running microtask chain can still starve macrotasks
Because the ENTIRE microtask queue must drain before any macrotask runs, a promise chain that keeps scheduling more microtasks (.then() returning another promise, repeatedly) can delay setTimeout callbacks and even UI rendering indefinitely. This is a real, if uncommon, performance pitfall โ€” macrotasks aren't starved by one slow microtask, but they can be starved by an unbounded chain of them.
QueueExamplesDrained when?
Call stackAll synchronous codeRuns first, completely, every time
Microtask queue.then(), catch(), finally(), code after awaitFully emptied before the next macrotask
Macrotask queuesetTimeout, setInterval, UI events, fetch's network completionOne at a time, after microtasks are clear

Coding Challenges

Challenge 1

Without running it, write down (or comment) the exact console.log order for this code, then run it to check: console.log("1"); setTimeout(() => console.log("2"), 0); Promise.resolve().then(() => console.log("3")); Promise.resolve().then(() => console.log("4")); console.log("5");

๐Ÿ“„ View solution
Challenge 2

Write an async function logSteps() that logs "1", then awaits a Promise.resolve(), then logs "2". Call logSteps(), then immediately log "3" right after the call (not inside it). Predict and then confirm the actual output order.

๐Ÿ“„ View solution
Challenge 3

Write code with TWO setTimeout calls (both 0ms, logging "timeout A" and "timeout B" in that order) and ONE promise .then() (logging "promise") scheduled between them. Run it and explain in a comment why the promise callback runs before EITHER timeout, despite being scheduled in the middle.

๐Ÿ“„ View solution

Chapter 3 Quick Reference

  • Call stack โ€” all synchronous code; always finishes completely first
  • Microtask queue โ€” promise .then()/catch()/finally(), code after await
  • Macrotask queue โ€” setTimeout, setInterval, UI events
  • Order: all sync code โ†’ entire microtask queue โ†’ ONE macrotask โ†’ check microtasks again โ†’ repeat
  • setTimeout(fn, 0) means "as soon as possible," NOT "immediately" โ€” sync code and microtasks always go first
  • await always yields to the microtask queue, even when the awaited value resolves instantly
  • An unbounded chain of microtasks can delay macrotasks indefinitely โ€” a real, rare performance pitfall
  • Next chapter: design patterns โ€” module pattern, observer, debounce/throttle
Chapter 4 of 5

Design Patterns

Course 3 ยท Ch 4
Design Patterns: Module Pattern, Observer, Debounce/Throttle
Three named, reusable solutions to problems that have already shown up, unnamed, in earlier chapters

A design pattern is just a recognised, named solution to a recurring problem โ€” none of the three covered here are new syntax; they're specific ways of combining things already learned (closures, callbacks, timers) to solve specific, common problems cleanly.

The Module Pattern โ€” Closures as Encapsulation

const CounterModule = (function() { let count = 0; // private โ€” never exposed directly return { increment() { count++; return count; }, reset() { count = 0; } }; })(); // IIFE โ€” called immediately, only the returned object survives console.log(CounterModule.increment()); // 1 console.log(CounterModule.count); // undefined โ€” no direct access

This is exactly Intermediate Chapter 2's createBankAccount private-state pattern, with one addition: an IIFE (Immediately Invoked Function Expression) โ€” the surrounding (function() { ... })() โ€” runs the function the instant it's defined, so CounterModule ends up holding the returned object directly, not the function itself. Before native ES modules (Chapter 1) existed, this was the standard way to create a private, self-contained unit of code.

Why "module pattern" if real modules now exist?
Native import/export (Chapter 1) has mostly replaced this pattern's original purpose โ€” but the underlying technique (closures hiding private state, exposing only a deliberate public interface) still shows up constantly inside individual files, classes, and libraries, regardless of module system.

The Observer Pattern โ€” One-to-Many Notifications

class EventEmitter { constructor() { this.listeners = {}; } on(event, callback) { if (!this.listeners[event]) this.listeners[event] = []; this.listeners[event].push(callback); } emit(event, data) { (this.listeners[event] || []).forEach(callback => callback(data)); } } const emitter = new EventEmitter(); emitter.on("login", user => console.log(`Welcome, ${user}`)); emitter.on("login", user => console.log(`Logging access for ${user}`)); emitter.emit("login", "Philip"); // Welcome, Philip // Logging access for Philip

The observer pattern lets multiple unrelated pieces of code (observers) react to something happening, without the thing that happened needing to know who's listening or how many there are. addEventListener (Fundamentals Chapter 8) is observer pattern, built into the browser โ€” EventEmitter here is the same idea, generalized beyond DOM events to any custom event name.

Debounce โ€” Wait Until Activity Stops

function debounce(fn, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => fn(...args), delay); }; } const search = debounce(query => console.log("Searching for:", query), 300); input.addEventListener("input", () => search(input.value)); // Typing "hello" fires the "input" event 5 times, but search() only RUNS once โ€” 300ms after the last keystroke

debounce wraps a function so it only actually runs once activity has genuinely stopped for delay milliseconds โ€” each new call cancels the previous pending timer and starts a fresh one. This is the standard fix for Fundamentals Chapter 9's "input" event firing on every keystroke when a search box should really only query once typing pauses.

Throttle โ€” Allow at Most Once Every X Milliseconds

function throttle(fn, limit) { let inCooldown = false; return function(...args) { if (inCooldown) return; fn(...args); inCooldown = true; setTimeout(() => inCooldown = false, limit); }; } const logScroll = throttle(() => console.log("Scroll position:", window.scrollY), 200); window.addEventListener("scroll", logScroll);

throttle guarantees the wrapped function runs AT MOST once per limit milliseconds, no matter how many times it's called in that window โ€” unlike debounce, the first call goes through immediately, and the function fires regularly during sustained activity rather than waiting for it to stop entirely. scroll events fire dozens of times per second; throttling keeps expensive work (logging, layout calculations) from running that often.

Debounce and throttle solve different problems โ€” don't mix them up
Debounce: "wait for quiet, then run once" โ€” right for search-as-you-type. Throttle: "run regularly, but cap the rate" โ€” right for scroll/resize handlers that need periodic updates throughout continuous activity, not just at the end.
PatternProblem it solvesBuilt from
Module patternPrivate state, public interfaceClosures + IIFE
ObserverMany listeners reacting to one event, decoupledArrays of callbacks + forEach
Debounce"Run once activity stops" (search-as-you-type)setTimeout + clearTimeout
Throttle"Run at most once per interval" (scroll/resize)setTimeout + a boolean flag

Coding Challenges

Challenge 1

Using the module pattern (IIFE returning an object), build a TodoModule with private state (an array) and a public interface of addTodo(text) and getAll(). Add 3 todos and log the result of getAll(), then confirm TodoModule.todos is undefined.

๐Ÿ“„ View solution
Challenge 2

Using the EventEmitter class from this chapter, create an instance and register two separate listeners for an "orderPlaced" event โ€” one logging a confirmation message, one logging that an email should be sent. Emit the event once with an order ID and confirm both listeners run.

๐Ÿ“„ View solution
Challenge 3

Using the debounce function from this chapter, wrap a function that logs "Saving..." with a 500ms delay. Call the debounced version 5 times in quick succession (e.g. in a loop with no delay between calls) and explain in a comment why "Saving..." only logs once.

๐Ÿ“„ View solution

Chapter 4 Quick Reference

  • Module pattern โ€” IIFE + closure, returning a deliberate public interface, hiding private state
  • IIFE: (function() { ... })() โ€” runs immediately, only what it returns survives
  • Observer pattern โ€” register listeners, emit events; addEventListener is this pattern, built-in
  • Debounce โ€” runs once, after activity stops for a set delay (search-as-you-type)
  • Throttle โ€” runs at most once per interval, during continuous activity (scroll/resize)
  • None of these are new syntax โ€” all three combine closures, timers, and callbacks already covered
  • Next chapter: working with APIs โ€” pagination, rate limits, caching strategies
Chapter 5 of 5

Working with APIs

Course 3 ยท Ch 5
Working with APIs: Pagination, Rate Limits, and Caching
The final chapter โ€” combining everything from fetch through generators into the patterns real production API code actually uses

Fundamentals Chapter 10 covered a single fetch call in isolation. Real APIs rarely hand back everything in one response โ€” they paginate, they rate-limit, and repeatedly re-fetching the same unchanged data wastes both time and the API's generosity. This final chapter brings together fetch, generators (Chapter 2), debounce (Chapter 4), and closures into the patterns that show up constantly in production code.

Pagination โ€” Fetching Data in Pages

async function fetchPage(pageNumber) { const response = await fetch(`https://api.example.com/posts?page=${pageNumber}&limit=10`); return response.json(); // { data: [...10 items...], hasMore: true } } async function fetchAllPages() { let page = 1; let allPosts = []; let hasMore = true; while (hasMore) { const result = await fetchPage(page); allPosts = [...allPosts, ...result.data]; hasMore = result.hasMore; page++; } return allPosts; }

Most APIs return data in pages to limit response size โ€” each call needs a page number, and a flag (here, hasMore) signals whether further pages exist. while (hasMore) keeps requesting and merging pages (using spread, Intermediate Chapter 1) until the API says there's nothing left.

A Generator Wrapping Pagination

async function* paginate(baseUrl) { let page = 1; let hasMore = true; while (hasMore) { const response = await fetch(`${baseUrl}?page=${page}`); const result = await response.json(); yield result.data; // hand back ONE page at a time, not all at once hasMore = result.hasMore; page++; } } for await (const pageOfPosts of paginate("https://api.example.com/posts")) { console.log("Got a page with", pageOfPosts.length, "items"); }

async function* combines Chapter 2's generators with async/await โ€” each yield hands back one page as soon as it's fetched, rather than waiting for every page to load before returning anything. for await (... of ...) is for...of's counterpart for these async generators, awaiting each yielded value automatically.

Rate Limiting โ€” Respecting an API's Request Caps

function createRateLimiter(maxPerSecond) { let queue = []; let processing = false; async function processQueue() { if (processing) return; processing = true; while (queue.length) { const { task, resolve } = queue.shift(); resolve(await task()); await new Promise(r => setTimeout(r, 1000 / maxPerSecond)); } processing = false; } return function limitedRequest(task) { return new Promise(resolve => { queue.push({ task, resolve }); processQueue(); }); }; } const limited = createRateLimiter(2); // max 2 requests per second for (let i = 1; i <= 5; i++) { limited(() => fetch(`https://api.example.com/item/${i}`)); }

Many APIs reject requests beyond a certain rate (e.g. "max 2 requests/second"). This rate limiter โ€” using Chapter 4's module pattern, a queue, and a spaced-out setTimeout delay between each item โ€” ensures requests are spread out automatically, regardless of how quickly the calling code tries to fire them off.

A 429 response means "slow down," not "broken"
HTTP status 429 Too Many Requests is the standard signal an API sends when its rate limit is exceeded. Production code should check for it specifically (response.status === 429) and back off โ€” retrying immediately just gets rejected again, and repeatedly hitting it can lead to a temporary or permanent IP ban from the API provider.

Caching โ€” Avoiding Repeated, Unnecessary Requests

function createCachedFetcher(ttlMs) { const cache = new Map(); return async function cachedFetch(url) { const cached = cache.get(url); if (cached && Date.now() - cached.timestamp < ttlMs) { return cached.data; // still fresh โ€” skip the network entirely } const response = await fetch(url); const data = await response.json(); cache.set(url, { data, timestamp: Date.now() }); return data; }; } const fetchWithCache = createCachedFetcher(60000); // cache for 60 seconds

Another module-pattern closure, this time holding a Map (a key/value structure similar to Go's map from the Go course, distinct from a plain object) keyed by URL. ttlMs ("time to live") decides how long a cached response stays valid โ€” repeated calls within that window return instantly from memory, with no network request at all.

ConcernTool/Pattern
Fetching multiple pageswhile loop checking a hasMore flag
Streaming pages one at a timeasync function* + for await...of
Respecting a rate limitA queue + spaced setTimeout delays between requests
Avoiding duplicate requestsA Map cache with a time-to-live check

Coding Challenges

Challenge 1

Write an async function fetchAllUsers() using the while-loop pagination pattern from this chapter, against https://jsonplaceholder.typicode.com/users (this particular API returns all results in one page, so simulate hasMore by stopping once the returned array's length is 0 on a fake "page 2"). Log the total count fetched.

๐Ÿ“„ View solution
Challenge 2

Write the createCachedFetcher function from this chapter exactly as shown. Create fetchWithCache with a 5000ms TTL, call it twice in a row with the same URL (https://jsonplaceholder.typicode.com/posts/1), and log a message indicating whether each call hit the cache or made a real network request.

๐Ÿ“„ View solution
Challenge 3

Write a function that checks a fetch Response object's status and, if it's 429, logs "Rate limited โ€” backing off" and waits 2 seconds (using a Promise + setTimeout) before returning a retry signal; otherwise it returns the response's parsed JSON normally. Test it with a mocked response object of { status: 429 }.

๐Ÿ“„ View solution

Chapter 5 Quick Reference

  • Pagination: loop while a hasMore-style flag is true, merging each page's results
  • async function* + for await...of โ€” stream paginated results one page at a time
  • 429 Too Many Requests โ€” the standard "you've hit the rate limit" HTTP status
  • Rate limiting: a queue + spaced delays, ensuring requests never exceed an allowed rate
  • Caching: a Map keyed by URL, with a timestamp + TTL check before reusing a cached value
  • All of these patterns combine closures, fetch, generators, and timers โ€” no new core syntax
  • This completes JavaScript Advanced and the JavaScript course overall as currently planned across all 3 courses.