JavaScript module 3: asynchrony and Node.js
Fourteen sessions for the turn that makes you a complete JavaScript programmer: asynchrony. How the language waits without ever freezing, promises and async/await, calling web APIs with fetch — then out of the browser: Node.js, npm, files, and a real little HTTP server written by hand. Method: 20 minutes of theory, 30 of practice, 10 of recap.
Day 29 — setTimeout and callbacks: programming in time
Goal: understand that a timer does not block the program, and that the output order surprises.
Theory (20 min)
So far, every line finished before the next began: synchronous code. But a real program sometimes has to wait — a timer, a network answer, a file. JavaScript refuses to freeze while waiting: it hands the waiting over to the environment and carries on, then picks your function back up when the wait is over. That function, handed over to be called back later, is a callback.
console.log("A");
setTimeout(() => {
console.log("B");
}, 1000);
console.log("C");
// Prints: A, C, then B one second later
Read the result carefully: B arrives AFTER C, even though B is written before C. setTimeout(fn, 1000) does not mean "sleep one second" but "in one second at the earliest, put fn back in the queue". The program immediately continues to C. Even with 0 milliseconds, B would come after C: a timer callback never runs before the current synchronous code has finished.
That is the asynchronous model: "do this when it is ready" instead of "wait until it is ready". A single thread of execution, which never stops — all of module 3 flows from that sentence. Callbacks are the historical form; tomorrow you will see the machine that orchestrates them, then the promises that make them bearable.
What will console.log(1); setTimeout(() => console.log(2), 0); console.log(3); print?
- 1, 2, 3
- 1, 3, 2 — but only with 1000 ms
- 1, 3, 2 — even with 0 ms
Answer
1, 3, 2: the callback joins the queue, and the queue is only served after the synchronous code, whatever the delay.
Practice (30 min)
- Retype the A/B/C example into
timers.js, run it with node: check the A, C, B order. - Change the delay to 0: the order does not change. Note it in the notebook — counter-intuitive and definitive.
setIntervalrepeats:const t = setInterval(() => console.log("tic"), 500);then stop after 3 tics withclearInterval(t)from a setTimeout of 1600 ms. Do it.- Success criterion: your script prints three "tic" spaced apart, then ends by itself (no Ctrl+C).
- Countdown: a setInterval that prints 3, 2, 1, "go", then stops. Use a let variable in the callback's closure — you can see day 16 at work.
Agent tips: "predict the output order of this script mixing console.log and setTimeout, line by line": a good test for YOUR mental model — the agent's is often right on this point. Trap: do not let it "fix" the order by moving lines; understand why first. Proof: the actual output, executed.
Recap (10 min)
Three lines: synchronous vs asynchronous, what setTimeout really promises, why 0 ms does not mean "right now". Without notes: write the script that prints "start", "end", then "after" half a second later.
Day 30 — The event loop: the kitchen, properly understood
Goal: form a correct picture of the event loop: stack, task queue, microtask queue.
Theory (20 min)
The big picture: a kitchen with ONE single cook. The cook (the JavaScript thread) does only one thing at a time, and never stands frozen in front of the oven: they start the baking, note the pending dish, and get on with something else. When the oven timer rings, they do not interrupt mid-gesture — they finish the gesture in progress, THEN fetch the dish.
Translated precisely:
- The call stack: the cook's gesture in progress — the function currently executing, and those waiting for it. As long as it is not empty, nothing else starts. That is why a
while (true)freezes everything: the cook never lets go of the gesture. - The environment's APIs: the oven and the timer — setTimeout, fetch, file reading. The waiting happens OUT of the cook's kitchen; it consumes none of their time.
- The task queue (macrotasks: setTimeout callbacks, network events): the list of dishes ready to be served.
- The microtask queue (promise callbacks,
.then,await): the list of urgent gestures slipped BETWEEN two dishes. The exact rule: when the stack empties, the engine FIRST drains the whole microtask queue, THEN takes one task. That is why a resolved promise runs before a setTimeout 0.
console.log("1");
setTimeout(() => console.log("2"), 0);
Promise.resolve().then(() => console.log("3"));
console.log("4");
// 1, 4, 3, 2 — synchronous first, microtasks next, tasks last
The event loop is the merry-go-round: stack empty? → drain the microtasks → take one task → repeat. All the fluidity of a web page or a Node server rests on this simple rule: never a long gesture, many small gestures interleaved.
What will setTimeout(() => console.log("t"), 0); Promise.resolve().then(() => console.log("p")); console.log("s"); print?
- s, p, t
- s, t, p
- t, p, s
Answer
s, p, t: synchronous, then microtask (promise), then macrotask (timer). The two-queue rule.
Practice (30 min)
- Retype the 1/4/3/2 example, run it, then move the lines around to produce three different orders that you PREDICTED in writing before each run. Success criterion: three exact predictions out of three.
- Demonstrate the blockage:
const start = Date.now(); while (Date.now() - start < 2000) {}followed by a console.log — two seconds of silence, then the rest. During that time, no callback gets through: the cook does not let go of the gesture. - Check that a setTimeout falling due DURING the blockage waits for the end: place a 500 ms setTimeout before the 2000 ms loop, measure its real delay (it will print around 2 s, not 0.5 s).
- In the notebook, draw from memory the three boxes (stack, microtasks, tasks) and the direction of circulation.
Agent tips: "here is a script mixing console.log, setTimeout and Promise.then; give the exact order and justify with the two-queue rule": the agent is reliable on this standard mechanism. Trap: if it invokes "parallelism", it is wrong — a single thread, never two gestures at once. Proof: the executed output, pasted back.
Recap (10 min)
Three lines: stack / microtasks / macrotasks and their serving order, why synchronous code always goes first, what freezes a page. Without notes: predict the order of a script combining all three, written by you.
Day 31 — Promises: a receipt for later
Goal: create a promise, and know its three states.
Theory (20 min)
A promise (Promise) is an object representing a result not yet available. The mental model: a dry-cleaner's receipt. You drop a shirt, you get a ticket (the promise), you carry on with your day. Later, two possible outcomes: the shirt is ready (the promise is fulfilled) or the shop calls you about a problem (it is rejected). In between, it is pending. And once fulfilled or rejected, it is final: a promise never changes state twice.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
const ok = true;
if (ok) {
resolve("shirt ready");
} else {
reject(new Error("impossible stain"));
}
}, 1000);
});
The constructor takes a function executed immediately, with two powers: resolve(value) fulfils the promise with that value, reject(error) rejects it. You collect the result with .then(value => ...) and the error with .catch(err => ...) — in detail tomorrow. Today, the key idea: a promise separates the moment you start the work from the moment you receive the result, and callbacks travel through clean channels (value on one side, error on the other) — gone the old "callback(err, result)" conventions.
Two instant factories: Promise.resolve(42) creates an already-fulfilled promise, Promise.reject(err) an already-rejected one — perfect for experiments.
Practice (30 min)
- Retype the dry-cleaner promise into
promise.js, addpromise.then(v => console.log("fulfilled:", v)).catch(e => console.log("rejected:", e.message));and run. "fulfilled: shirt ready" after one second. - Flip
okto false: the error takes the catch channel. Success criterion: both paths displayed at least once each. - Create
const wait = ms => new Promise(resolve => setTimeout(resolve, ms));— the promise that "sleeps" ms milliseconds. Test it:wait(500).then(() => console.log("awake"));This wait function will serve you all module long. - Play with the states:
Promise.resolve(7).then(v => console.log(v * 6));prints 42 immediately (in a microtask — after the synchronous code, you know that). - Check unicity: call resolve then reject in the same constructor, observe that only the first counts.
Agent tips: "convert this callback code (setTimeout) into a promise": a classic, safe conversion. Trap: the agent may forget the reject path — without it, an error becomes infinite silence. Proof: a deliberate failure run must take the catch.
Recap (10 min)
Three lines: the three states and their irreversibility, resolve/reject as two channels, the receipt analogy. Without notes: write the wait(ms) function from memory and use it.
Day 32 — then, catch, finally: chaining promises
Goal: chain asynchronous steps cleanly, and propagate errors.
Theory (20 min)
The strength of promises: .then() returns a NEW promise, fulfilled with whatever your callback returned. Steps therefore chain:
wait(300)
.then(() => "step 1")
.then(text => text + " → step 2")
.then(chain => console.log(chain));
// "step 1 → step 2"
Three rules cover chaining. (1) What the callback returns becomes the next step's value — one forgotten return, and the next step receives undefined (day 12's trap returns). (2) If the callback returns a PROMISE, the chain waits for it: that is how real slow operations are chained, without nesting. (3) A throw (or a rejection) skips all steps until the next catch — exactly like try…catch, but across time.
wait(100)
.then(() => { throw new Error("failure at step 2"); })
.then(() => console.log("never runs"))
.catch(err => console.log("caught:", err.message))
.finally(() => console.log("in all cases"));
finally runs on success or failure — for cleanup (closing, resetting). The old "callback hell" was functions nested in functions; chaining unfolds it into a vertical list of steps, each readable. Remember the shape: one line per step, a single catch at the bottom, which sees every failure.
Practice (30 min)
- Retype the course's two examples, run them, check the announced outputs.
- Build the chain: wait 200 ms → yield 21 → double it → print 42. Success criterion: "42" prints, and if you remove the doubling's return, the next step receives undefined — check it on purpose, then fix.
- Propagate an error: three thens, a throw in the second, a catch at the bottom. Observe the third then is skipped.
- Put a then back AFTER the catch: it runs (the catch "absorbed" the error and returned a fulfilled promise). Note the nuance in the notebook: catch swallows, finally observes.
- A realistic mini-scenario: simulate "download then process then save" with three chained waits and step console.logs.
Agent tips: "unfold this callback hell into a then chain": an agent flattens well. Trap: it may leave promises un-returned — every step that awaits something must RETURN the promise. Proof: the step console.logs print in order, not all at once.
Recap (10 min)
Three lines: then returns a promise, the return rule, the skip-to-catch. Without notes: write the chain "wait, yield a text, lengthen it, print it".
Day 33 — async / await: asynchrony that reads like synchrony
Goal: write asynchronous code with async/await, and catch its errors with try…catch.
Theory (20 min)
async/await (2017) is the modern, definitive form. Two keywords: async in front of a function makes it return a promise; await in front of a promise, INSIDE an async function, suspends that function until the result — without blocking anyone else.
async function makeCoffee() {
console.log("water on the stove");
await wait(1000); // local pause, kitchen not blocked
console.log("coffee poured");
return "ready";
}
makeCoffee().then(msg => console.log(msg));
What really happens (and you know it since day 30): at the await, the function suspends and hands control back; the rest of it is stored as a microtask that will resume when the promise fulfils. The code READS top to bottom like synchronous code, but executes in dashes. It is sugar over promises: async/await adds no new mechanics, it makes the same mechanics readable.
Errors get try…catch back, day 21's comfort:
async function mission() {
try {
const a = await step1();
const b = await step2(a);
return b;
} catch (err) {
console.log("failed:", err.message);
}
}
Two traps to know today: await outside an async function is a SyntaxError (except at the top level of an ESM module — day 43); and forgetting await in front of a promise leaves you handling the promise instead of its value — if your "result" prints Promise { <pending> }, that is it.
const p = makeCoffee(); console.log(p); prints what?
- "ready"
- Promise { <pending> }
- an error
Answer
An async function returns a promise, always. You need await or .then to see "ready".
Practice (30 min)
- Take your
waitfrom day 31. Rewrite day 32's "download, process, save" scenario in async/await, with step console.logs. Compare the two versions' readability in the notebook. - Success criterion: the steps print in sequence, spaced out, and the async function returns a summary that you print with .then.
- Trigger an error in step 2 (throw), catch it with try…catch around the awaits. The program survives and says so.
- Forget an await on purpose: print the value, observe the
Promise { <pending> }, fix it. That is the error you will meet most this year. - Wrap the call: a
main()async function orchestrating everything, launched bymain().catch(...)as the last line — the standard pattern for Node scripts.
Agent tips: "rewrite this then chain as async/await": a reliable conversion. Trap: the agent may serialise what could run in parallel (tomorrow) — three awaits in a row means three queues. Proof: the observed behaviour (order and total duration) is identical.
Recap (10 min)
Three lines: async returns a promise, await suspends without blocking, try…catch regained. Without notes: write an async function that waits 500 ms then returns your first name, and print it.
Day 34 — Promise.all and its siblings: several tasks at once
Goal: launch operations in parallel and pick the right combinator.
Theory (20 min)
Three awaits in a row is a queue: each task waits for the previous one to finish. But if the tasks are independent (three network requests with no link between them), launch them AT THE SAME TIME and await the group. The combinators:
Promise.all([p1, p2, p3]): succeeds when ALL have succeeded, with the array of results in the promises' order (not arrival order). If ONE fails, all fails immediately with that error.Promise.allSettled([...]): waits for EVERYONE, successes and failures, and returns an array of{ status: "fulfilled", value }or{ status: "rejected", reason }objects. The right choice when each result matters individually (a dashboard, backups).Promise.race([...]): the first to finish wins, success or failure. Use: timeouts.Promise.any([...]): the first SUCCESS wins; fails only if all fail. Use: several mirrors of the same data.
const [weather, news] = await Promise.all([getWeather(), getNews()]);
// both requests left together; total wait = the slowest, not the sum
The key distinction when writing: const p1 = f(); const p2 = g(); await Promise.all([p1, p2]) launches first, awaits after — whereas await f(); await g(); chains. Launch, THEN await: that is the whole difference, and it shows on the stopwatch.
Practice (30 min)
- With your
wait: measure the time of two 500 ms awaits in series (≈ 1 s), then ofPromise.all([wait(500), wait(500)])(≈ 0.5 s). UseDate.now()before/after. Success criterion: both measurements written in the notebook prove the parallelism. - Check the results' order: all with a slow promise first, a fast one second — the array of values respects the writing order.
- Trigger a failure: all with a rejected one among three. all fails; switch to allSettled and explore the status array: two fulfilled, one rejected.
- Write a reusable timeout:
const timeout = (p, ms) => Promise.race([p, wait(ms).then(() => { throw new Error("too slow"); })]);Test it on a short wait then a long one. - Scenario: three simulated "sensors" (random wait + value), print the three readings together when all are in, then the first arrival with race.
Agent tips: "can these four calls run in parallel? if not, which depends on what?": dependency analysis is a good use. Trap: the agent may parallelise calls where one needs another's result. Proof: the measured total duration drops, and the results stay correct.
Recap (10 min)
Three lines: launch then await, all vs allSettled vs race vs any, the order of all's results. Without notes: rewrite the timeout with Promise.race.
Day 35 — fetch: fetching data from the web
Goal: call a public API with fetch and extract its JSON.
Theory (20 min)
fetch(url) requests a resource from the network and returns a promise of a response. It is available in browsers AND in Node 18+: the same code on both sides. Two asynchronous steps, two awaits:
const response = await fetch("https://api.open-meteo.com/v1/forecast?latitude=45.76&longitude=4.83¤t=temperature_2m");
const data = await response.json();
console.log(data.current.temperature_2m);
Why two steps: the first await waits for the response headers (status, type); the body is still arriving over the network, and .json() is a second promise that reads it in full and parses it (day 22's JSON.parse, made asynchronous). This example calls Open-Meteo, a free weather API with no key or account: Lyon's latitude/longitude as parameters, current temperature in return.
Two disciplines right now. (1) A URL carries its parameters in the query string (?key=value&key2=value2) — build it cleanly, URLSearchParams arrives tomorrow. (2) An API is read in its documentation: which parameters, what response shape. The Open-Meteo response is an object with a current key, itself holding temperature_2m: explore it in the console with Object.keys before writing blind access paths.
Practice (30 min)
- In a file
weather.js, wrap the course's code in an async main() function launched bymain().catch(e => console.error("failed:", e.message));Run it: Lyon's current temperature prints. - Success criterion: a number prints, probably different from yesterday — this is real network. Check its plausibility (between -20 and 45, say).
- Explore the response: print
Object.keys(data)thendata.currentin full. Spot the unit indata.current_units. - Change the city: your city's coordinates (search "city name latitude longitude"), rerun, compare.
- Ask for one more field in the query:
¤t=temperature_2m,relative_humidity_2m(a comma between fields), print both.
Agent tips: paste an excerpt of the real JSON response and ask "write the access path to that field". Trap: without the excerpt, the agent invents the structure — it "knows" the API as of two years ago. Proof: the path run on today's response gives a value, not undefined.
Recap (10 min)
Three lines: fetch's two awaits, the query string, why explore the response before writing paths. Without notes: write the full weather call (fetch → json → print the temperature).
Day 36 — fetch in practice: HTTP errors and timeouts
Goal: handle the three ways a fetch can go wrong — and build clean URLs.
Theory (20 min)
A fetch can fail in three distinct ways, and the first surprises everyone:
- The network is down (no connection, nonexistent domain): the promise REJECTS. Your catch sees it.
- The server answers an error (404, 500): the promise SUCCEEDS anyway! fetch does not reject on an HTTP error status — it is up to you to test
response.ok(true for statuses 200-299) orresponse.status. Forgetting this check is THE classic fetch bug: you parse the body of an error page and crash further on, mysteriously. - The server never answers: fetch waits indefinitely by default. The fix:
AbortController, a timer that cancels the request.
const control = new AbortController();
const timer = setTimeout(() => control.abort(), 5000);
try {
const response = await fetch(url, { signal: control.signal });
clearTimeout(timer);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
} catch (err) {
console.log("failed:", err.message);
}
For URLs, URLSearchParams encodes parameters correctly (spaces, accents, &) instead of gluing them by hand:
const params = new URLSearchParams({ latitude: "45.76", longitude: "4.83", current: "temperature_2m" });
const url = `https://api.open-meteo.com/v1/forecast?${params}`;
Practice (30 min)
- Take day 35's weather.js and add the
response.okcheck with the course's error message. - Trigger a 404: replace the path with
/v1/forecasttt. Success criterion: your program prints "failed: HTTP 404" cleanly, instead of crashing on missing JSON. - Trigger a simulated network failure: a nonexistent domain (
https://api.nonexistent.example/). Observe the catch receives a network error, different from the 404 — the course's two channels, seen live. - Add the AbortController at 5000 ms, then set it to 1 ms: the abort error (
AbortError) arrives immediately. Set a reasonable value back. - Refactor the URL with URLSearchParams and add a
timezone: "auto"parameter.
Agent tips: "write a getJSON(url) function with a status check and a 5 s timeout": an agent produces this pattern cleanly. Trap: it may omit the response.ok check — the most common bug in generated code. Proof: demand and run the deliberate-404 test.
Recap (10 min)
Three lines: the three failure modes and who detects each, AbortController, URLSearchParams. Without notes: write getJSON(url) complete, from memory.
Day 37 — Node.js: running a script, process.argv
Goal: leave the browser for good: Node scripts, command-line arguments, process.
Theory (20 min)
Since day 1, you have been running files with node file.js — that is already Node. Today, what changes: your code reaches the machine. The global process object represents the running program: process.argv holds the command line's words (an array: node's path, the script's path, then YOUR arguments), process.env the environment variables, process.exit(1) ends the program signalling a failure.
// hello.js
const name = process.argv[2] ?? "stranger";
console.log(`Hello ${name}!`);
$ node hello.js Ada
Hello Ada!
argv's first two elements are always the engine and the script: your arguments start at index 2. Hence the idiom process.argv.slice(2) to keep only yours. A missing argument is undefined — remember the defaults with ?? (day 10).
Node also brings built-in modules (the standard library): node:fs for files, node:http for the network, node:path for paths, node:test for tests (module 4). The node: prefix says explicitly "this is built in, not a package". Import them with the modern ESM syntax: import { readFile } from "node:fs/promises"; — it works in a .mjs file, or in a .js of a project declared ESM (days 38 and 43).
Practice (30 min)
- Write the course's
hello.js, run it with your first name, then with no argument: the default takes over. Success criterion: both runs print a correct message. - Write
sum.jsadding all its numeric arguments:node sum.js 10 20 12prints 42. Remember Number() (day 4: argv is made of strings) and slice(2). - Fail cleanly: with no argument,
sum.jsprints a usage message (usage: node sum.js numbers…) and ends withprocess.exit(1). Check the exit code:echo $?(Linux/macOS) prints 1. - Explore process: print
process.version,process.platform, and an environment variable (process.env.HOMEon Linux/macOS). - Rename one of your scripts to
.mjsand importnode:fs/promisesat the top — just to confirm the ESM syntax passes (the content arrives on day 39).
Agent tips: "write a Node CLI taking two arguments and doing X; usage printed and exit 1 if an argument is missing": a pattern well known to agents. Trap: generated code may forget Number() on argv — everything there is a string. Proof: node sum.js 1 2 must print 3, not "12".
Recap (10 min)
Three lines: argv and its index 2, exit(1) as a failure signal, the node: modules and the ESM import. Without notes: write the script that greets the name passed as an argument, with a default.
Day 38 — npm and package.json
Goal: initialise a project, understand package.json, and install a package knowingly.
Theory (20 min)
npm is the package manager installed with Node: a store of over a million libraries, and the tool that shelves them for you. A project's heart is its package.json: the identity card (name, version, module type) and the list of dependencies — the packages the project needs.
mkdir tools && cd tools
npm init -y
npm init -y creates a default package.json. Add "type": "module" to it: it declares that your .js use the ESM syntax (import/export), the modern one — without it, Node expects the old require syntax. npm install the-package downloads the package into node_modules/ (the folder to NEVER version) and records it in dependencies; package-lock.json freezes the exact versions so any reinstall gives the same content.
The discipline that avoids 90% of npm misery: every dependency is a stranger's code running on your machine. Before installing: is the package maintained (last update?), widely downloaded, and above all needed? Half of common needs (dates, files, tests, HTTP) are covered by built-in modules — this path installs almost nothing, and that is a choice. The honest command to know: npm audit lists your dependencies' known vulnerabilities.
Practice (30 min)
- Create the
toolsproject with npm init -y, open package.json, add"type": "module". Note in the notebook the role of each visible field. - Install a tiny, useful-for-the-exercise package:
npm install cowsay(yes, the talking cow — a harmless classic for learning the gesture). - Use it in
cow.mjs:import cowsay from "cowsay";thenconsole.log(cowsay.say({ text: "npm works" }));Run it. Success criterion: the ASCII cow says "npm works". - Read package.json after the install: cowsay appeared in dependencies, package-lock.json exists, node_modules weighs a few megabytes.
- Prove reproducibility: delete node_modules, then
npm install(with no name) restores everything from the lock. Run the cow again. - Run
npm auditand read the output, even if empty.
Agent tips: "I need X; does Node's built-in module cover it, or do I need a package?": demand the "built-in" answer first. Trap: an agent suggests a package out of reflex. Proof: the official Node doc page covering the need.
Recap (10 min)
Three lines: package.json / package-lock / node_modules and their roles, "type": "module", why a dependency is a trust decision. Without notes: create a project, declare it ESM, install a package.
Day 39 — Reading and writing files with fs/promises
Goal: read, write and store files with the asynchronous versions of node:fs.
Theory (20 min)
node:fs/promises gives file operations as promises — compatible with your day 33 async/await:
import { readFile, writeFile, mkdir } from "node:fs/promises";
const text = await readFile("notes.txt", "utf8");
await writeFile("copy.txt", text.toUpperCase(), "utf8");
await mkdir("archives", { recursive: true });
Three details separate a working script from a reliable one. (1) Always the "utf8" encoding when reading text: without it, readFile returns a Buffer (raw bytes) and your string methods do not apply. (2) Errors are rejections: missing file → ENOENT, denied rights → EACCES. Wrap in try…catch and print err.code: it is more precise than the message. (3) Paths: your script resolves relative paths from the folder where you RUN node, not from the script's folder — always run from the project root, or build paths with node:path.
The JSON reflex now assembles itself: JSON.parse(await readFile("data.json", "utf8")) reads structured data, writeFile("out.json", JSON.stringify(object, null, 2)) saves it readable. You hold half of real automation scripts.
Practice (30 min)
- In the
toolsproject (already ESM), createnotes.txtwith three lines of text, thenread.mjswhich reads it and prints its line count (split("\n").length). - Success criterion: the printed count is exact; then rename the file and rerun: your catch prints
ENOENTcleanly instead of a crash stack. - Write
backup.mjs: an object of your choice → indented JSON written toarchives/data.json(mkdir recursive first), read back and printed as proof. - Word counter: read notes.txt, split into words, count occurrences with your day 19 reduce, write the result as JSON. Your first real utility script.
- Test the launch-folder trap: run the script from the parent with
node tools/read.mjsand observe the ENOENT — the relative path changes meaning.
Agent tips: "write a script that reads data.json and prints X": trivial for an agent. Trap: it may use readFileSync "to simplify" — demand the async version, that is the habit to build. Proof: the script runs, and the deliberate ENOENT is caught.
Recap (10 min)
Three lines: utf8 or Buffer, err.code vs message, paths relative to the launch folder. Without notes: write the script that copies a text file in uppercase.
Day 40 — A tiny HTTP server with node:http
Goal: write a server that listens on a port and answers every request.
Theory (20 min)
A web server is a program that listens on a port of the machine (a numbered door; 3000 by convention in development) and runs YOUR function on each incoming request. Node does it with nothing to install, via node:http:
import { createServer } from "node:http";
const server = createServer((req, res) => {
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Hello from my server!\n");
});
server.listen(3000, () => {
console.log("Listening on http://localhost:3000");
});
Let us dissect: createServer takes the callback that will be called for EVERY request — day 30's event loop in full action: the server waits without blocking, and your function is called back on each visit. req describes the request (method in req.method, path in req.url); res builds the response: headers with writeHead, body with end (which closes the response — a forgotten end and the browser waits forever). localhost means your own machine; http://localhost:3000 is only visible at home.
While the server runs, the terminal is busy — that is normal, it is listening. Stop it with Ctrl+C. To test it: another browser tab, or curl http://localhost:3000 in a second terminal.
Practice (30 min)
- Write the course's
server.mjs, run it, open http://localhost:3000 in the browser. Success criterion: the message shows in the browser, and each refresh is one more request. - Add a visit counter: a let variable at the top, incremented in the callback, shown in the response. Refresh: the counter climbs — your server has in-memory state.
- Print
req.methodandreq.urlin the terminal console, then visithttp://localhost:3000/path/test?x=1: watch the details arrive. - Answer differently per path:
/greets,/timegives the time (new Date().toISOString()), everything else gets a 404 withres.writeHead(404). Test all three. - Forget res.end on purpose, reload: the browser spins forever. Fix it — the reflex is engraved.
Agent tips: "write a node:http server with two routes, no framework": an agent knows this pattern. Trap: it may produce an example with require() (old syntax) or with unrequested Express. Proof: the file runs with node, in ESM, with no dependency.
Recap (10 min)
Three lines: port, request/callback/response, res.end's role. Without notes: rewrite the hello server from memory and visit it.
Day 41 — Serving JSON: routes and responses
Goal: turn the server into a mini-API: clean routes, JSON, status codes.
Theory (20 min)
Yesterday's server answered text. An API answers structured data — JSON, which you have known since day 22. The pattern is always the same: parse req.url, route (that is routing), produce an object, serialise it:
import { createServer } from "node:http";
const tasks = [
{ id: 1, title: "learn reduce", done: true },
{ id: 2, title: "write an API", done: false }
];
const server = createServer((req, res) => {
const url = new URL(req.url, "http://localhost");
if (url.pathname === "/tasks") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(tasks));
return;
}
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "unknown route" }));
});
server.listen(3000);
Three points of care. (1) new URL(req.url, ...) parses cleanly: url.pathname gives the path without the query, url.searchParams.get("x") the parameters — the same URLSearchParams as day 36, server side. (2) Content-Type: application/json tells the client what it receives. (3) The status is part of the response: 200 success, 404 unknown route, 400 malformed request. A well-raised client (your day 36 fetch, with its ok check) relies on it: the loop is closed, you are now on both sides.
For routing, one condition per route and a return after each response suffice at this level. Frameworks (Express, Fastify…) only mechanise what you just wrote by hand — you will understand them when the day comes.
Practice (30 min)
- Write the course's tasks API, run it.
curl http://localhost:3000/tasksprints the JSON;curl -i http://localhost:3000/othershows the 404. - Success criterion: both responses carry the right status AND the right Content-Type — check with
curl -i, which shows the headers. - Add the route
/tasks?id=1: searchParams, find (day 24), and a 404 if the id does not exist. - Add
/stats:{ total: 2, done: 1 }computed on the array with filter — your module 2 pipeline serving an API. - Call your own API with fetch from a second Node script (async main, ok check, json): the client and the server are both yours.
Agent tips: "add a /stats route to this server, following the existing style": an agent extends a visible pattern well. Trap: it may bring in a "light" framework along the way — refuse, the exercise is to stay with node:http. Proof: curl -i on each route, statuses checked.
Recap (10 min)
Three lines: routing by pathname, Content-Type and status, the server/client symmetry. Without notes: write the one-route API that returns the time as JSON.
Day 42 — Module 3 review
Goal: consolidate asynchrony and Node before the module of tests and the final project.
Theory (20 min)
Module 3 changed your status: you now write programs that wait without freezing, talk to the network on both sides, and touch files. Oral self-test: "synchronous code always goes first", "microtasks drain before the next task", "a promise is a receipt with three states", "await suspends without blocking", "fetch does not reject on a 404", "launch then await", "argv starts at index 2", "always utf8".
Module 4 answers the question that naturally follows: how to be SURE all this code does what it says? Answer: automated tests, written by you, run in one command. Then we assemble everything — language, asynchrony, Node, tests — into the final project: a command-line tool that queries a real API and summarises its response, tested and documented.
A colleague writes: const rep = await fetch(url); const data = await rep.json(); return data.current.temperature_2m; — with no rep.ok check. What do they risk?
- nothing, fetch rejects on HTTP errors
- on a 500, they parse an error body and crash on undefined further on, far from the cause
Answer
fetch does not reject on error statuses: the ok check before json() is mandatory.
Practice (30 min)
- The orders exam: write a script mixing console.log, setTimeout 0, Promise.resolve().then and an await; predict the order in writing, run, check. Success criterion: exact prediction.
- Rewrite from memory: the wait(ms) function, getJSON(url) with status check, the two-route node:http server. Three files, three successful runs.
- Integrating mini-challenge (15 min): a script that reads a list of cities from a JSON file, calls Open-Meteo for each IN PARALLEL (Promise.all + your coordinates hard-coded in the file), and writes a JSON report sorted by temperature. Everything is already learned; assemble.
- Tidy up:
js/module-3/, notebook up to date, and list what still needs a re-read.
Agent tips: "review my mini-challenge and list the potential errors: missing await, unchecked ok, relative path": give it a checklist, not a fix. Trap: a full rewrite deprives you of the lesson. Proof: you fix each flagged point yourself, then rerun.
Recap (10 min)
Three lines: the event loop's mechanism in one sentence, fetch's three failure modes, what you want to prove with module 4's tests. The home straight begins.
Found this useful? Tell us what to measure next.
Comments
No account, no tracker: a comment is read by a human before it appears.