diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 9229f24..ed6a00a 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -1,4 +1,4 @@ -# GitHub Copilot Instructions for astro-cody-blog +# GitHub Copilot Instructions for blog - This is an Astro 5 blog template with MDX content, client-side React UI, and static output. The project is designed for minimal runtime APIs: all page data is built at compile time via `astro:content` and `src/lib/content.ts`. - Source-of-truth site metadata: `src/config/site.ts`. Categories are in `src/config/categories.ts`. Do not hardcode author/site settings elsewhere. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..6870a9b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,135 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +```bash +npm run dev # start dev server +npm run build # astro build + pagefind index (must run together) +npm run preview # preview the production build locally + +npm run typecheck # astro sync + tsc --noEmit +npm run lint # eslint src +npm run lint:fix # eslint src --fix +npm run format # prettier write +npm run format:check # prettier check + +npm run test # vitest run (single pass) +npm run test:watch # vitest watch mode +npm run buildcheck # full pre-deploy: typecheck + lint + format + test + build +``` + +Run a single test file: +```bash +npx vitest run src/lib/__tests__/content.test.ts +``` + +## Architecture + +### Stack +Astro (static output) + Preact (with React compat, acts as React) + Tailwind v4 (vite plugin). All pages are pre-rendered at build time — there is no SSR. + +### Content Collections +Two collections defined in `src/content/config.ts`: + +- **`blog`** — regular posts at `src/content/blog/{category}/{slug}.mdx`. Frontmatter fields: `title`, `date` (display string), `pubDate` (Date), `category`, `summary`, `tags`, `draft`, `isVisible`, `featured`, `relatedPosts`, `prevPost`, `nextPost`. +- **`series`** — at `src/content/series/{seriesSlug}/`. Each series has: + - `index.md` — series metadata with a `postOrder: [slug1, slug2, ...]` array that defines sequence and controls which posts appear + - Individual `.mdx` files — one per post in the series + +`draft: true` hides a post from all listings. `isVisible: false` hides from listings but the post page still renders (used for unlisted/hidden posts). + +### Component Zones +- `src/components/astro/` — server-rendered Astro components (Navbar, Footer, CategoryBadge, Note, etc.) +- `src/components/mdx/` — Astro components intended for import inside MDX post bodies (Callout, Steps, Step, NetworkPath, SequenceDiagram, ArchFlow, Terminal, PacketDiagram, etc.) +- `src/components/react/` — interactive Preact components (PostsGrid, SearchModal, SeriesAccordion, TableOfContents, ContactForm, etc.) + +**`src/lib/content.ts` is server-side only** — it uses `astro:content` which is a build-time virtual module. Never import it in React/Preact components. Client-side data comes from the static JSON API below. + +### Static JSON API +`src/pages/api/posts/[category]/[page].json.ts` and `src/pages/api/series/[category]/[page].json.ts` are pre-rendered at build time into paginated JSON files. PostsGrid and SeriesAccordion fetch these client-side rather than receiving all data as props. `slugifyCategory()` in `src/config/categories.ts` is the single source of truth for category → URL slug conversion — it's used at both build time (endpoint generation) and runtime (fetch calls). + +### Configuration Files +- `src/config/site.ts` — all site identity, URLs, social links, API endpoints, analytics, comments (Giscus). Single place to edit when personalising the blog. +- `src/config/categories.ts` — `CATEGORY_CONFIG` array is the source of truth for category display names, badge colours, and homepage visibility. Categories not in this config still work as filters but render with fallback styling and won't appear on the homepage cards. + +### Build Pipeline +`npm run build` runs `astro build` followed by `npx pagefind --site dist`. Pagefind generates the search index post-build, so it is not available during the Astro build phase. The service worker (via `@vite-pwa/astro`) precaches build output but leaves pagefind assets to runtime caching only. + +### OG Images +Generated at build time using Satori + `@resvg/resvg-js` via `src/pages/og/blog/[...slug].png.ts` and `src/pages/og/series/[seriesSlug]/[postSlug].png.ts`. + +### Reading Time +Injected at build time by the `src/lib/remark-reading-time.ts` remark plugin into post frontmatter. `resolveReadingTime()` in `content.ts` falls back to a word-count estimate when the plugin value is absent. + +### Testing +Vitest with `astro:content` aliased to `src/__mocks__/astro-content.ts` (the real module is unavailable outside the Astro build). All tests live in `src/lib/__tests__/`. Tests cover pure utility functions only — no Astro component tests. + +### Code Syntax Highlighting +Shiki dual-theme (`github-dark` / `github-light`) emits `--shiki-dark` / `--shiki-light` CSS variables per token. `global.css` switches between them based on `[data-theme]` on ``. + +## GitHub Issue & PR Workflow + +This repo uses GitHub issues to track every blog post and series. The GitHub MCP (`mcp__github__*` tools) is connected and must be used for all GitHub operations — do not use the `gh` CLI. + +### Labels +Three labels are used (create them at `github.com/honeycoder96/blog/labels` if missing): + +| Label | Colour | Purpose | +|-------|--------|---------| +| `epic` | `#6B46C1` | Top-level tracking issue for a series | +| `javascript-promises` | `#F0DB4F` | All issues in the JS Promises series | +| `how-internet-works` | `#0EA5E9` | All issues in the How Internet Works series | + +### New series → create an epic +When a new series is started, create one epic issue using `mcp__github__issue_write` with `method: "create"`: + +- **Title:** `Epic: {Series Title} ({N} posts)` +- **Labels:** `["epic", "{series-label}"]` +- **Body:** series description, a structure table (parts/blocks with post counts), and a story checklist section (populate with issue links once stories exist) + +After creating all story issues, update the epic body (`method: "update"`, `issue_number: `) to replace the placeholder checklist with `- [ ] #{issue} Post NN: Title` lines grouped by part/block. + +### New post → create a story issue +For each new post being written, create an issue using `mcp__github__issue_write` with `method: "create"`: + +- **Title:** `Post NN: {Post Title}` +- **Labels:** `["{series-label}"]` (or no label for standalone blog posts) +- **Body:** + ``` + **Epic:** #{epic-issue-number} + **Part/Block:** {part or block name} + **Series order:** NN of total + + ## Description + {one-paragraph description of what the post covers} + + ## Key topics + - bullet list of the main concepts to cover + ``` + +### Post published → close the story issue +When a post's `.mdx` file is written and committed, close its issue: + +``` +mcp__github__issue_write + method: "update" + issue_number: {N} + state: "closed" + state_reason: "completed" +``` + +The epic's checklist item auto-renders as checked once the issue is closed. + +### PR body → reference the issue +Every PR for a post must reference its issue in the PR body so GitHub auto-links them and marks the issue as in-progress: + +``` +## Summary +- Writes Post NN: {title} + +Closes #{issue-number} +``` + +Use `mcp__github__create_pull_request` with `head: "stage"`, `base: "main"`. The `Closes #N` line is what triggers the auto-link to the project board. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ee6eed..9867f49 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,8 +49,8 @@ The following are **out of scope** for pull requests: ```bash # 1. Fork the repo on GitHub, then: -git clone https://github.com//astro-cody-blog.git -cd astro-cody-blog +git clone https://github.com//blog.git +cd blog # 2. Install dependencies npm install diff --git a/README.md b/README.md index ec68808..1779ce3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# astro-cody-blog +# Blog A minimal, fast, dark-mode-first engineering blog built with Astro 5, React, Tailwind CSS v4, and Pagefind. Clone it, edit one config file, and you have your own blog. @@ -25,8 +25,8 @@ A minimal, fast, dark-mode-first engineering blog built with Astro 5, React, Tai ```bash -git clone https://github.com/your-username/astro-cody-blog.git -cd astro-cody-blog +git clone https://github.com/your-username/blog.git +cd blog npm install cp .env.example .env.development npm run dev diff --git a/package.json b/package.json index 7e17178..2acd7ec 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "astro-cody-blog", + "name": "blog", "type": "module", "version": "0.0.1", "scripts": { diff --git a/src/content/series/javascript-promises/callbacks.mdx b/src/content/series/javascript-promises/callbacks.mdx new file mode 100644 index 0000000..091836b --- /dev/null +++ b/src/content/series/javascript-promises/callbacks.mdx @@ -0,0 +1,343 @@ +--- +isVisible: true +title: "Callbacks — The Original Async Pattern" +date: "Mar 2026" +pubDate: 2026-03-20 +category: JavaScript +author: Honey Sharma +summary: Callbacks were JavaScript's first answer to async programming. They worked — until they didn't. This post covers how callbacks actually work, why they collapse under complexity, and the two deeper problems that no amount of refactoring can fix. +featured: false +tags: [javascript, promises, callbacks, async, js-interview] +--- +import Note from '../../../components/astro/Note.astro' + +--- + +The event loop, as we saw in the last post, delivers async results by queuing tasks. A timer fires — its callback is queued. A network response arrives — its callback is queued. But we glossed over something: *how do you tell the event loop which function to queue?* + +You pass it. You hand a function to the async operation, and that operation calls it when it's done. That function is a callback. + +It is the simplest possible answer to async. Pass a function, get it called later. For a long time, it was the only answer JavaScript had. + +## What a Callback Actually Is + +The word "callback" makes people think "async". That association is wrong, and it matters to untangle it. + +A callback is just a function passed as an argument to another function, which then calls it. The mechanism is completely ordinary: + +```js +// These are all callbacks. None of them are async. + +[3, 1, 2].sort((a, b) => a - b); +// (a, b) => a - b is called synchronously by sort, many times + +[1, 2, 3, 4].filter(n => n % 2 === 0); +// n => n % 2 === 0 is called synchronously by filter, once per element + +document.querySelectorAll('p').forEach(el => el.classList.add('highlight')); +// el => ... is called synchronously by forEach, once per element +``` + +Nothing async has happened here. A function was passed in, and the receiving function called it. That's the whole pattern. + +What changes when the receiving function is async — a timer, a network request, a file read — is *when* your callback gets called. Not now. Later, when the operation completes and the event loop picks it up from the task queue. + +```js +// The environment holds this callback until 1000ms passes. +// Then it queues it. The event loop runs it when the stack is clear. +setTimeout(function onTimeout() { + console.log('one second later'); +}, 1000); + +// XHR: callback runs when the server responds +const xhr = new XMLHttpRequest(); +xhr.addEventListener('load', function onLoad() { + console.log(this.responseText); +}); +xhr.open('GET', '/api/data'); +xhr.send(); +``` + +The JS thread isn't blocked waiting. The environment tracks these operations independently, outside the JS engine. When they complete, their callbacks land in the task queue and the event loop delivers them to the call stack. + +This is genuinely elegant. A single-threaded language handles I/O without freezing because it hands the waiting to the environment and stays free to do other things. Callbacks are what make that handoff possible. + + +Callbacks aren't an anti-pattern. Event listeners, array methods, timers, observer patterns — callbacks are the right tool for all of these. The problems emerge specifically when you need to *compose* multiple async operations: sequencing them, error handling across them, or running several in parallel. + + + +## Sequencing: The First Sign of Trouble + +A single async callback is fine. The problem arrives the moment one async operation depends on the result of another. + +Say you need to: read a config file, then fetch a user using the ID from that config, then save a report based on what you fetched. Three async steps. Each one needs the result of the previous. + +```js +// Step 1: read the config +fs.readFile('./config.json', 'utf8', function(err, configData) { + const config = JSON.parse(configData); + + // Step 2: fetch the user — but only once we have config.userId + fetchUser(config.userId, function(err, user) { + const report = buildReport(user); + + // Step 3: save the report — but only once we have the user data + fs.writeFile('./report.txt', report, function(err) { + console.log('report saved'); + }); + }); +}); +``` + +Three levels. Still readable. The indentation mirrors the dependency: each step is nested inside the one it waits for. If you squint, the code structure looks like a flowchart. + +But this is already the shape that will cause problems. There's no other way to express "do B after A, then C after B" with callbacks — you must nest. Each new dependent step adds a level. + + +## Callback Hell + +Now add the requirements a real feature actually has. Authenticate the user first. Check whether they have permission to run this report. Validate the data before writing. Log the access for the audit trail. + +```js +authenticate(credentials, function(authErr, token) { + if (authErr) { handleError(authErr); return; } + + getUser(token, function(userErr, user) { + if (userErr) { handleError(userErr); return; } + + checkPermissions(user, 'generate:report', function(permErr, allowed) { + if (permErr) { handleError(permErr); return; } + if (!allowed) { handleError(new Error('Forbidden')); return; } + + fetchReportData(user.id, function(fetchErr, data) { + if (fetchErr) { handleError(fetchErr); return; } + + validateData(data, function(validErr, isValid) { + if (validErr) { handleError(validErr); return; } + if (!isValid) { handleError(new Error('Invalid data')); return; } + + generateReport(data, function(genErr, report) { + if (genErr) { handleError(genErr); return; } + + saveAuditLog(user.id, 'report:generated', function(logErr) { + if (logErr) console.error('Audit log failed:', logErr); + // non-fatal, so we continue regardless + + res.json({ report }); + }); + }); + }); + }); + }); + }); +}); +``` + +This is callback hell. Not because someone wrote careless code. Because eight sequential async operations, each dependent on the previous result, each requiring error handling, simply produce this shape with callbacks. There is no other way to write them. + +The visual indentation — the rightward drift, the triangular pyramid — is what gives it the name. But the shape is the least serious problem. This code is also: + +- **Unmaintainable** — inserting a step means restructuring the entire tree, touching every indentation level below it +- **Untestable** — the inner callbacks are anonymous functions buried inside closures; you can't reach them directly +- **Opaque in failure** — `handleError` is called from eight different places; a stack trace tells you it was called, not which step triggered it +- **Error-prone** — every `if (err) { handleError(err); return; }` block is a place where a missing `return` silently continues execution into the next level with `undefined` data + + +You can flatten this structure by pulling each callback out into a named top-level function. That removes the indentation and makes the code readable line-by-line. What it does not fix are the two deeper problems in the next two sections — those are structural, not cosmetic. + + + +## Inversion of Control: The Trust Problem + +The nesting problem is a style problem. This one is not. + +When you pass a callback to a function you didn't write — a third-party library, an SDK, a framework hook — you hand that function control over your code. You are saying: *call this function for me, when you think the time is right.* The receiving function now owns: + +- **When** your callback is called +- **How many times** it is called +- **Whether** it is called at all +- **What arguments** it receives +- **What happens** if it throws + +Consider a payment flow. The outcome of this callback determines whether you fulfil an order. + +```js +paymentProvider.charge(cart.total, function onCharged(err, receipt) { + if (err) { + showError('Payment failed.'); + return; + } + + // At this point you believe the charge succeeded. + fulfillOrder(); + sendConfirmationEmail(); + updateInventory(); +}); +``` + +You've handed `onCharged` to `paymentProvider.charge`. You trust it will be called exactly once, after the charge attempt completes, with either an error or a valid receipt. + +Here are five things that could go wrong — all caused by bugs in the library, not in your code: + +```js +// 1. Called twice +// A retry logic bug in the library calls onCharged on both the initial +// response and the retry response. fulfillOrder() runs twice. +// The customer receives two orders and is charged once. + +// 2. Never called +// A network timeout causes the library to swallow the result silently. +// The charge may have gone through on the server. You'll never know. +// The customer is waiting. Your server gives up. + +// 3. Called synchronously +// The library has a fast-path for cached payment instruments that resolves +// immediately, synchronously. onCharged fires before .charge() returns. +// Any code after .charge() that assumes the callback hasn't run yet is broken. + +// 4. Exception swallowed +// fulfillOrder() throws an exception — a database error, a null reference. +// The library wraps the callback call in try/catch and discards the throw. +// From your perspective, the payment succeeded and the order was never fulfilled. +// No error is reported anywhere. + +// 5. Called with unexpected arguments +// On certain failure modes, the library passes both err and receipt. +// Your if (err) check doesn't return early because receipt is also truthy. +// fulfillOrder() runs on a failed payment. +``` + +Every one of these bugs has shipped in real payment libraries. Some processors have had all five at different points in their history. + +You cannot defend against any of them from inside your callback. You are not in control. The library is. + + +This is not fixable with better callback code. The problem is the model itself: you've given your code to someone else to execute. The only solution is a system where *you* hold a handle to the result and attach your response code to it — on your own terms, not the async operation's. That is precisely what Promises are designed to provide. + + + +## Error-First Callbacks: The Attempted Standard + +Node.js recognised the error handling chaos and established a convention: every async callback receives `(err, result)` as its first two arguments. If something went wrong, `err` is a non-null `Error` object. If the operation succeeded, `err` is `null` and `result` holds the value. + +```js +fs.readFile('./data.json', 'utf8', function(err, data) { + if (err) { + console.error('Read failed:', err.message); + return; + } + + // err is null here — safe to use data + console.log(JSON.parse(data)); +}); +``` + +This was a genuine improvement. Errors are explicit, predictable, always in the same position. You can write a linter rule to warn when the first argument is ignored. The entire Node.js standard library follows it. + +But three problems remain. + +**Problem 1 — The missing `return`** + +```js +fetchUser(id, function(err, user) { + if (err) { + logError(err); + // Missing return. Execution falls through. + } + + // This runs even when err is non-null. user is undefined. + renderProfile(user); // TypeError: Cannot read properties of undefined +}); +``` + +Nothing in the language catches this. There's no enforcement. Forget the `return` and you get two code paths running simultaneously: error handling *and* the success path. The resulting bugs are often subtle — silent failures or corrupted state rather than an obvious crash. + +**Problem 2 — Errors thrown inside your callback** + +The error-first convention handles errors *delivered to* your callback. It says nothing about errors *thrown inside* it. + +```js +fs.readFile('./data.json', 'utf8', function(err, data) { + if (err) return handleError(err); + + const parsed = JSON.parse(data); // Throws SyntaxError if data is malformed + renderTemplate(parsed); // Throws TypeError if parsed is missing expected fields +}); +``` + +What happens to those thrown errors depends entirely on whether `fs.readFile`'s implementation wraps your callback in a `try/catch`. In most cases it does not. The exception propagates up to whatever invoked the event loop task, becomes an uncaught exception, and crashes the process — or, in a browser, disappears into the void. + +**Problem 3 — Still vulnerable to inversion of control** + +Error-first is a calling convention. It doesn't address who controls when or how many times your callback is called. A library that follows error-first perfectly can still call your callback twice, never call it, or swallow exceptions it throws. The trust problem is orthogonal to the argument layout. + +
+What does this code output? Think before expanding. + +```js +function loadUser(id, callback) { + setTimeout(function() { + if (id <= 0) { + callback(new Error('Invalid ID')); + } + callback(null, { id, name: 'Alice' }); + }, 0); +} + +loadUser(1, function(err, user) { + if (err) { + console.log('Error:', err.message); + return; + } + console.log('User:', user.name); +}); +``` + +**Output: `User: Alice`** — for `id: 1`, which is valid. But `loadUser` has a bug: when `id <= 0`, it calls `callback(new Error('Invalid ID'))` and then — because there's no `return` — falls through and calls `callback(null, { id, name: 'Alice' })` as well. The callback runs twice. If you change `id` to `-1`, you'll see both `Error: Invalid ID` and `User: Alice` printed in sequence. The bug is in the *producer* of the callback, not the consumer — and as a consumer, you have no way to detect or prevent it. + +
+ + +## What Callbacks Cannot Fix + +Pull it all together. The problems callbacks create fall into two categories. + +**Structural problems** — these are real, but addressable with discipline: + +| Problem | Mitigation | +|---------|------------| +| Rightward pyramid drift | Name your callbacks and hoist them to top-level functions | +| Hard to read sequential flow | Sequence named functions in comments | +| Variables leaking across closure levels | Limit what each callback closes over | + +**Fundamental problems** — these are not addressable without changing the model: + +| Problem | Why callbacks can't fix it | +|---------|---------------------------| +| Inversion of control | The async operation owns your callback — you can't change that | +| No single-call guarantee | Nothing prevents a callback being called zero times or many times | +| Sync/async inconsistency | A callback may fire now or later; you can't know without reading the source | +| No error propagation | Thrown errors inside callbacks don't travel up the chain — they vanish or crash | +| No way to compose | You can't `return` a value, can't use try/catch across steps, can't use a for loop to sequence async operations | + +The structural problems are a developer experience issue. The fundamental problems are bugs waiting to happen. + + +## Why This Matters + +Callbacks failed not because developers misused them, but because the model itself has unavoidable limitations. The core issue is ownership: when you pass a function to an async operation, that operation owns your code. It can call it however it likes. You have no recourse. + +What if the model were reversed? What if the async operation, instead of taking your callback, handed *back* a value — a placeholder representing a result that doesn't exist yet? You'd hold that value. You'd attach your response code to it. When the async operation completed, it would fill the placeholder — and your code would run. + +You would be in control. Not the async operation. + +That is exactly what a Promise is. A value that represents an eventual result. One you hold. One you attach handlers to on your own terms. + +That's what the next post is about. + +## Further Reading + +- [Callback Hell](http://callbackhell.com/) — the canonical reference on the structural problem and the named-function mitigation +- [You Don't Know JS: Async & Performance — Chapter 2](https://github.com/getify/You-Dont-Know-JS/blob/1st-ed/async%20%26%20performance/ch2.md) — Kyle Simpson's treatment of the inversion of control and trust problems in detail +- [Designing APIs for Asynchrony](https://blog.izs.me/2013/08/designing-apis-for-asynchrony/) — Isaac Schlueter's original post naming the Zalgo problem (sync/async inconsistency) and why it matters diff --git a/src/content/series/javascript-promises/index.md b/src/content/series/javascript-promises/index.md index 697c1d9..defe0c1 100644 --- a/src/content/series/javascript-promises/index.md +++ b/src/content/series/javascript-promises/index.md @@ -6,4 +6,5 @@ order: 2 postOrder: - promises-in-depth - before-promises + - callbacks --- diff --git a/src/content/series/javascript-promises/promises-in-depth.mdx b/src/content/series/javascript-promises/promises-in-depth.mdx index 6384dc1..3b3162e 100644 --- a/src/content/series/javascript-promises/promises-in-depth.mdx +++ b/src/content/series/javascript-promises/promises-in-depth.mdx @@ -33,9 +33,9 @@ Before writing a single line of Promise code, it's worth understanding what worl | # | Post | Description | |---|------|-------------| -| 01 | [The World Before Promises](#) | The JavaScript event loop, the call stack, and what it means to be single-threaded | -| 02 | [Callbacks — The Original Async Pattern](#) | How callbacks work, callback hell, and the inversion of control problem | -| 03 | [What Promises Set Out to Solve](#) | The mental model of a future value, and the history from jQuery Deferreds to the spec | +| 01 | [The World Before Promises](/series/javascript-promises/before-promises) | The JavaScript event loop, the call stack, and what it means to be single-threaded | +| 02 | [Callbacks — The Original Async Pattern](/series/javascript-promises/callbacks) | How callbacks work, callback hell, and the inversion of control problem | +| 03 | What Promises Set Out to Solve | The mental model of a future value, and the history from jQuery Deferreds to the spec | --- @@ -45,10 +45,10 @@ The beating heart of the series. This is where you learn exactly how a Promise w | # | Post | Description | |---|------|-------------| -| 04 | [Anatomy of a Promise](#) | The three states, one-way state transitions, the executor, and what resolve/reject actually do | -| 05 | [Consuming Promises with `.then()`](#) | The `.then()` signature, return values, why it always returns a new promise, and microtask scheduling | -| 06 | [Error Handling and Cleanup — `.catch()` and `.finally()`](#) | How rejections propagate, recovering mid-chain, unhandled rejections, and `.finally()` gotchas | -| 07 | [Promise Chaining In Depth](#) | Sequential async pipelines, thenable flattening, common chaining mistakes, and flat vs nested chains | +| 04 | Anatomy of a Promise | The three states, one-way state transitions, the executor, and what resolve/reject actually do | +| 05 | Consuming Promises with `.then()` | The `.then()` signature, return values, why it always returns a new promise, and microtask scheduling | +| 06 | Error Handling and Cleanup — `.catch()` and `.finally()` | How rejections propagate, recovering mid-chain, unhandled rejections, and `.finally()` gotchas | +| 07 | Promise Chaining In Depth | Sequential async pipelines, thenable flattening, common chaining mistakes, and flat vs nested chains | --- @@ -58,11 +58,11 @@ Every static method on the `Promise` constructor deserves its own focused treatm | # | Post | Description | |---|------|-------------| -| 08 | [`Promise.resolve()` and `Promise.reject()`](#) | Pre-settled promises, wrapping thenables, and the identity shortcut | -| 09 | [`Promise.all()`](#) | Parallelising independent operations, fail-fast behaviour, and ordering guarantees | -| 10 | [`Promise.allSettled()`](#) | Running promises regardless of individual failures and building resilient multi-fetch patterns | -| 11 | [`Promise.race()`](#) | First-settled-wins semantics, timeout wrappers, and why losing promises aren't cancelled | -| 12 | [`Promise.any()`](#) | First-fulfilled-wins, `AggregateError`, and how to choose between `any`, `race`, and `all` | +| 08 | `Promise.resolve()` and `Promise.reject()` | Pre-settled promises, wrapping thenables, and the identity shortcut | +| 09 | `Promise.all()` | Parallelising independent operations, fail-fast behaviour, and ordering guarantees | +| 10 | `Promise.allSettled()` | Running promises regardless of individual failures and building resilient multi-fetch patterns | +| 11 | `Promise.race()` | First-settled-wins semantics, timeout wrappers, and why losing promises aren't cancelled | +| 12 | `Promise.any()` | First-fulfilled-wins, `AggregateError`, and how to choose between `any`, `race`, and `all` | --- @@ -72,7 +72,7 @@ Every static method on the `Promise` constructor deserves its own focused treatm | # | Post | Description | |---|------|-------------| -| 13 | [async/await — The Full Picture](#) | What `async` functions return, how `await` suspends execution, translating between chains and await, accidental serialisation in loops, the `await` in `forEach` trap, floating promises, and top-level await | +| 13 | async/await — The Full Picture | What `async` functions return, how `await` suspends execution, translating between chains and await, accidental serialisation in loops, the `await` in `forEach` trap, floating promises, and top-level await | --- @@ -82,11 +82,11 @@ The practical half of the series. Real patterns used in real codebases, explaine | # | Post | Description | |---|------|-------------| -| 14 | [Execution Patterns and Concurrency Control](#) | Sequential tasks with `reduce`, parallelising with `Promise.all`, and building a promise pool / concurrency limiter from scratch | -| 15 | [Retry, Timeout, and Fallback Patterns](#) | Exponential backoff, timeout wrappers with `Promise.race`, circuit breakers, and waterfall fallbacks | -| 16 | [Deferred Pattern and Promise Factories](#) | Separating resolve/reject from the executor, lazy promises, and memoising async calls | -| 17 | [Cancellation — The Gap Promises Leave](#) | Why the spec has no cancellation, AbortController in practice, and cooperative cancellation in chains | -| 18 | [Promisifying Callback-based APIs](#) | Writing `promisify` by hand, `util.promisify` in Node.js, and promisifying event emitters | +| 14 | Execution Patterns and Concurrency Control | Sequential tasks with `reduce`, parallelising with `Promise.all`, and building a promise pool / concurrency limiter from scratch | +| 15 | Retry, Timeout, and Fallback Patterns | Exponential backoff, timeout wrappers with `Promise.race`, circuit breakers, and waterfall fallbacks | +| 16 | Deferred Pattern and Promise Factories | Separating resolve/reject from the executor, lazy promises, and memoising async calls | +| 17 | Cancellation — The Gap Promises Leave | Why the spec has no cancellation, AbortController in practice, and cooperative cancellation in chains | +| 18 | Promisifying Callback-based APIs | Writing `promisify` by hand, `util.promisify` in Node.js, and promisifying event emitters | --- @@ -96,8 +96,8 @@ Promises are powerful, but they have real constraints. This part is the honest a | # | Post | Description | |---|------|-------------| -| 19 | [Promise Limitations and Pitfalls](#) | No cancellation, no progress, eager execution, silently swallowed rejections, and mixing `.then(null, fn)` with `.catch()` incorrectly | -| 20 | [Memory and Performance Considerations](#) | Microtask queue pressure, long chains vs flat await, and avoiding memory leaks with settled promises | +| 19 | Promise Limitations and Pitfalls | No cancellation, no progress, eager execution, silently swallowed rejections, and mixing `.then(null, fn)` with `.catch()` incorrectly | +| 20 | Memory and Performance Considerations | Microtask queue pressure, long chains vs flat await, and avoiding memory leaks with settled promises | --- @@ -107,12 +107,12 @@ The payoff. By this point in the series, you have enough context to read the Pro | # | Post | Description | |---|------|-------------| -| 21 | [The Promises/A+ Specification — A Deep Read](#) | What the spec covers, the resolution procedure `[[Resolve]](promise, x)` line by line, and the microtask requirement | -| 22 | [Scaffolding the Polyfill — State and Executor](#) | Representing state with closures, running the executor safely, and implementing single-fire resolve/reject | -| 23 | [Implementing `.then()`](#) | Creating the chained promise, queueing pending handlers, and scheduling with `queueMicrotask` | -| 24 | [Implementing the Resolution Procedure](#) | Thenable detection, adopting the state of returned promises, and guarding against circular resolution | -| 25 | [Adding `.catch()`, `.finally()`, and Static Methods](#) | Deriving instance methods from `.then()` and implementing all four static combinators | -| 26 | [Testing and Validating the Polyfill](#) | Running the official Promises/A+ compliance suite, fixing edge cases, and comparing against native `Promise` | +| 21 | The Promises/A+ Specification — A Deep Read | What the spec covers, the resolution procedure `[[Resolve]](promise, x)` line by line, and the microtask requirement | +| 22 | Scaffolding the Polyfill — State and Executor | Representing state with closures, running the executor safely, and implementing single-fire resolve/reject | +| 23 | Implementing `.then()` | Creating the chained promise, queueing pending handlers, and scheduling with `queueMicrotask` | +| 24 | Implementing the Resolution Procedure | Thenable detection, adopting the state of returned promises, and guarding against circular resolution | +| 25 | Adding `.catch()`, `.finally()`, and Static Methods | Deriving instance methods from `.then()` and implementing all four static combinators | +| 26 | Testing and Validating the Polyfill | Running the official Promises/A+ compliance suite, fixing edge cases, and comparing against native `Promise` | --- @@ -122,10 +122,10 @@ These posts are written specifically for interview preparation. Each one is self | # | Post | Description | |---|------|-------------| -| 27 | [Predict the Output — Promise Execution Order](#) | A curated set of tricky snippets involving microtasks, chaining, and async/await — with deep explanations of every output | -| 28 | [The Most Common Promise Interview Questions](#) | The questions that come up repeatedly, answered properly — states, what `.then()` returns, difference between `all` and `allSettled`, and more | -| 29 | [Implement These in an Interview — Promise Utilities](#) | Walking through live implementations of `promisify`, `retry`, a concurrency limiter, and `Promise.all` from scratch | -| 30 | [async/await vs Promises — Interview Edition](#) | How to articulate the relationship clearly, what interviewers are actually testing, and the common misconceptions to avoid | +| 27 | Predict the Output — Promise Execution Order | A curated set of tricky snippets involving microtasks, chaining, and async/await — with deep explanations of every output | +| 28 | The Most Common Promise Interview Questions | The questions that come up repeatedly, answered properly — states, what `.then()` returns, difference between `all` and `allSettled`, and more | +| 29 | Implement These in an Interview — Promise Utilities | Walking through live implementations of `promisify`, `retry`, a concurrency limiter, and `Promise.all` from scratch | +| 30 | async/await vs Promises — Interview Edition | How to articulate the relationship clearly, what interviewers are actually testing, and the common misconceptions to avoid | ---