# Learn JavaScript > Free JavaScript tutorials with a runnable console, plus AI-native workflow guides for JS projects. Canonical: https://learn-javascript.org/ Licence: content free to read and quote with attribution to Learn JavaScript (https://learn-javascript.org/). Maintainer: Code Learning Dojo. Last built 2026-09-06. ## Foundations The syntax and the mental model. Short, runnable, no fluff. - [Hello, World!](https://learn-javascript.org/hello-world/): Welcome to the first tutorial. In this tutorial you will learn how to write your first line of code. JavaScript is a very powerful language. - [Variables and Types](https://learn-javascript.org/variables-and-types/): JavaScript is dynamically typed, so a variable can hold any value. Declare with const by default, let when you need to reassign. - [Arrays](https://learn-javascript.org/arrays/): JavaScript can hold an array of variables in an Array object. In JavaScript, an array also functions as a list, a stack or a queue. - [Manipulating Arrays](https://learn-javascript.org/manipulating-arrays/): Arrays can also function as a stack. The push and pop methods insert and remove variables from the end of an array. - [Operators](https://learn-javascript.org/operators/): Every variable in JavaScript is casted automatically so any operator between two variables will always give some kind of result. - [Conditions](https://learn-javascript.org/conditions/): The if statement allows us to check if an expression is equal to true or false, and execute different code according to the result. - [Loops](https://learn-javascript.org/loops/): JavaScript has two methods for running the same code several times. It is mainly used for iterating over arrays or objects. - [Objects](https://learn-javascript.org/objects/): JavaScript is a functional language, and for object oriented programming it uses both objects and functions, but objects are usually used as a data structure, similar to a dictionary in Python or a map in Java. - [Functions](https://learn-javascript.org/functions/): Functions are code blocks that can have arguments, and function have their own scope. - [Pop-up Boxes](https://learn-javascript.org/pop-up-boxes/): There are three types of pop-up boxes in javascript: confirm, alert, and prompt. - [Callbacks](https://learn-javascript.org/callbacks/): Callbacks in JavaScript are functions that are passed as arguments to other functions. - [Arrow Functions](https://learn-javascript.org/arrow-functions/): Arrow functions are a feature of ES6, their behavior are generally the same of a function. - [Object Oriented JavaScript](https://learn-javascript.org/object-oriented-javascript/): JavaScript uses functions as classes to create objects using the new keyword. - [Function Context](https://learn-javascript.org/function-context/): Functions in JavaScript run in a specific context, and using the this variable we have access to it. - [Inheritance](https://learn-javascript.org/inheritance/): JavaScript uses prototype based inheritance. - [Destructuring](https://learn-javascript.org/destructuring/): Destructuring is a feature of ES6, introduced for making easier and cleaner some repetitive operations and assignments made in JS. - [Promises and async/await](https://learn-javascript.org/promises-and-async-await/): The single most important topic in modern JavaScript, and the one where generated code most often gets it subtly wrong. - [Modules](https://learn-javascript.org/modules/): How JavaScript code is split across files — and the CommonJS/ESM split that still causes more confusion than anything else in the ecosystem. - [Error Handling](https://learn-javascript.org/error-handling/): Throwing, catching, and the specific ways JavaScript error handling goes wrong — including the one that terminates your process. - [Closures and Scope](https://learn-javascript.org/closures-and-scope/): The mechanism behind callbacks, module privacy, React hooks and most memory leaks. Worth understanding properly once. - [TensorFlow.js](https://learn-javascript.org/tensorflow-js/): TensorFlow.js is an open-source hardware-accelerated JavaScript library for training and deploying machine learning models. - [Convolutional Neural Networks with TensorFlow.js](https://learn-javascript.org/tensorflow-cnns/): Master computer vision with CNNs using TensorFlow.js. - [TensorFlow.js Transfer Learning Tutorial](https://learn-javascript.org/tensorflow-transfer-learning/): Transfer learning allows you to take a pre-trained model and adapt it for your specific use case with minimal training data. - [Brain.js](https://learn-javascript.org/brain-js/): Brain.js is a GPU accelerated JavaScript library for Neural Networks. - [GPU Acceleration with Brain.js](https://learn-javascript.org/brain-js-gpu/): Learn how to leverage GPU power for faster neural network training and inference using Brain.js GPU acceleration features. - [Recurrent Neural Networks with Brain.js](https://learn-javascript.org/brain-js-rnns/): Master sequence learning with RNNs and LSTMs using Brain.js. Perfect for text generation, time series prediction, and sequential data analysis. - [ml5.js](https://learn-javascript.org/ml5-js/): ml5.js is a friendly JavaScript library for the browser that makes machine learning accessible to artists and creative coders. - [ml5.js + Teachable Machine Integration](https://learn-javascript.org/ml5-teachable-machine/): Learn how to create custom machine learning models with Google's Teachable Machine and integrate them seamlessly with ml5.js for powerful web applications. - [Creative Coding with ml5.js + p5.js](https://learn-javascript.org/ml5-creative-coding/): Explore the intersection of machine learning and creative coding. - [Creative Coding with ml5.js + p5.js](https://learn-javascript.org/ml5-creative-coding-p5/): Explore the intersection of machine learning and creative coding. ## AI-Native Configuring agents, harnesses and feedback loops for this language. Updated as the tooling moves. - [Setting up a coding agent for a JavaScript project](https://learn-javascript.org/ai/agent-setup/): Node projects have the loosest defaults of any ecosystem here, which means the setup work matters more. - [Writing an AGENTS.md for JavaScript](https://learn-javascript.org/ai/agents-md/): JavaScript needs a longer instructions file than the typed languages, and most of the extra length should be one list: what the platform now does that a package used to. - [Testing JavaScript that calls a language model](https://learn-javascript.org/ai/evals/): Your function is now non-deterministic, slow, costs money per call, and streams. Here is a test strategy that still works — including for the streaming part, which is where most of the real bugs are. - [Tracking and cutting token costs in Node](https://learn-javascript.org/ai/tokenomics/): Node's specific cost problem is streaming: the user leaves, the stream keeps running, and you keep paying. That plus three other changes usually halves the bill. ## Review & Verify How generated code fails in this language, and the checks that catch it before your users do. - [The JavaScript mistakes language models actually make](https://learn-javascript.org/review/failure-modes/): Async is where most of it lives. The rest is stale idioms from a decade of training data written before the language got good. - [npm dependency hygiene: install scripts, slopsquatting and transitive bloat](https://learn-javascript.org/review/dependencies/): npm is the largest supply-chain surface in software, and the only major ecosystem where installing a package runs arbitrary code before you have written a line. - [Security review checklist for AI-generated JavaScript](https://learn-javascript.org/review/security/): No compiler, a dynamic object model and the largest dependency ecosystem in software. The vulnerabilities that recur in generated JS follow from all three. - [The performance traps in generated JavaScript](https://learn-javascript.org/review/performance/): Node is single-threaded, so a performance bug is an availability bug. Every item here is really the same item: something blocked the event loop or ran unbounded. ## Reference pages - [About Learn JavaScript, and how we make money](https://learn-javascript.org/about/): Editorial policy, sourcing, corrections and affiliate disclosure for Learn JavaScript, part of the Code Learning Dojo network. - [The JavaScript stack we would set up today](https://learn-javascript.org/tools/): An opinionated JavaScript toolchain for 2026: runtime, package manager, linting, testing, and the dependencies you no longer need. --- # Full text ## Hello, World! Source: https://learn-javascript.org/hello-world/ Welcome to the first tutorial. In this tutorial you will learn how to write your first line of code. JavaScript is a very powerful language. It can be used within any browser in the world. On top of that, it can be used to write server-side code using node.js. When using JavaScript inside the browser, we can change how the page looks like and how it behaves. In this tutorial, we will only focus on learning the language itself, and therefore we will only use one function to print out our results called “console.log”. ## Setting up a coding agent for a JavaScript project Source: https://learn-javascript.org/ai/agent-setup/ JavaScript gives an agent the least resistance of any language on this network. There is no compiler, the runtime accepts almost anything, and a wrong program usually runs until it does not. That makes the setup below more important here than anywhere else — you are building the feedback the language declines to provide. ## 1. Turn on type checking, even without TypeScript The single highest-value change, and it does not require renaming a file. ```json jsconfig.json { "compilerOptions": { "checkJs": true, "strict": true, "noEmit": true, "target": "ES2023", "module": "nodenext", "moduleResolution": "nodenext" }, "include": ["src/**/*.js"] } ``` ```bash npx tsc --noEmit ``` You now get real type errors on plain JavaScript, driven by JSDoc annotations and inference. More importantly, it unlocks the type-aware ESLint rules — including `no-floating-promises`, which catches the most common bug in generated JavaScript. See [the failure modes](/review/failure-modes/). Expect errors on existing code. Add `// @ts-nocheck` at the top of the worst files, fix inwards, and delete the pragmas as you go. ## 2. The lint config that catches real bugs ```js eslint.config.js import js from "@eslint/js"; import ts from "typescript-eslint"; export default [ js.configs.recommended, ...ts.configs.recommendedTypeChecked, { languageOptions: { parserOptions: { projectService: true } }, rules: { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/await-thenable": "error", "require-atomic-updates": "error", "no-param-reassign": ["error", { props: true }], "no-await-in-loop": "warn", "eqeqeq": ["error", "smart"], }, }, ]; ``` The type-checked preset is the part that matters. Without type information, ESLint cannot tell a promise from an object, and the async rules — where the real bugs are — do not work. ## 3. Four named commands ```json package.json { "scripts": { "typecheck": "tsc --noEmit", "lint": "eslint . --fix", "test": "vitest run", "check": "npm run typecheck && npm run lint && npm run test" } } ``` `npm run check` is the contract. Make sure it genuinely fails on a broken tree before you rely on it. ## 4. A fast test loop ```js vitest.config.js export default { test: { include: ["src/**/*.test.js"], exclude: ["**/*.integration.test.js"], // default run stays offline pool: "threads", }, }; ``` The threshold to aim for is five seconds. Below it, the agent runs tests after every edit and converges. Above thirty, it stops running them and starts telling you the code "should work". ## 5. Permissions ```json .claude/settings.json { "permissions": { "allow": [ "Bash(npm run test:*)", "Bash(npm run lint)", "Bash(npm run check)", "Bash(npx tsc:*)", "Bash(git status)", "Bash(git diff:*)" ], "ask": ["Bash(npm install:*)", "Bash(pnpm add:*)", "Bash(git push:*)"], "deny": ["Read(./.env)", "Read(./.env.*)", "Bash(curl:*)", "Bash(rm -rf:*)"] } } ``` Package installation belongs behind a confirmation. npm is the largest slopsquatting target of any registry, and models invent plausible package names — a two-second glance at the name before it installs is the cheapest control you have. ## 6. AGENTS.md, focused on what the language will not enforce ```markdown AGENTS.md Node 22, ESM only, pnpm. jsconfig.json has checkJs: true — keep it passing. ## Commands - Check: `pnpm check` (tsc --noEmit, eslint, vitest run). Must pass. - One test: `pnpm vitest run src/thing.test.js` ## Conventions - await every promise. Floating promises are errors. - Promise.all for independent work. A sequential loop needs a comment saying why. - ?? for defaults, not || — 0 and "" are valid values. - Do not mutate arguments, props or state. structuredClone for deep copies. - Every fetch in a React effect aborts on cleanup. ## Use the platform, not a package fetch (not axios), crypto.randomUUID (not uuid), node --env-file (not dotenv), Intl/Temporal (not moment), fs.rm (not rimraf), Object.groupBy (not lodash). ## Landmines - src/legacy/ is CommonJS and is loaded by the worker. Do not convert it. ``` That last section — "use the platform" — is worth including in every JavaScript project. Models write the 2018 dependency by default because that is what the training data contains, and the dependencies accumulate silently. The full version of this file, with the reasoning for each line, is in [writing an AGENTS.md for JavaScript](/ai/agents-md/). :::promo frontendmasters ::: ## What this costs Agent sessions are billed, and the bill is driven by context size more than by how much you ask for. The levers — prompt caching, pruning unused MCP servers, starting a fresh session when the task changes — are in [what tokens actually cost](https://codelearningdojo.com/token-economics/). ## The checklist ```text [ ] jsconfig.json with checkJs, and tsc --noEmit passing [ ] type-aware ESLint with no-floating-promises [ ] pnpm check fails on a broken tree [ ] default test run under 5 seconds, offline [ ] install commands behind a confirmation [ ] AGENTS.md with the "use the platform" list ``` ## Common questions ### Is `checkJs` really worth it on an existing codebase? Yes, and you do not have to fix everything at once. `// @ts-nocheck` on the worst files gets you a passing build immediately, and every file you remove the pragma from is a file the agent now gets real type feedback on. The unlock for the async lint rules alone justifies it. ### ESLint or Biome? Biome is much faster and covers formatting plus a good rule set. But the rules that catch the expensive bugs here need type information, and today that means ESLint. A reasonable setup is Biome for formatting and fast rules, ESLint for the type-aware ones. ### Should the agent be allowed to install packages? Behind a confirmation, always. npm has the largest supply-chain attack surface of any registry and models produce plausible-but-nonexistent package names at a measurable rate. Reading the name before it installs takes two seconds. ## The JavaScript mistakes language models actually make Source: https://learn-javascript.org/review/failure-modes/ JavaScript's problem is the opposite of Go's. There is no compiler to refuse anything, the language has changed enormously over fifteen years, and the training data spans all of it. So generated JavaScript is usually correct and frequently written in the dialect of 2016. The bugs cluster in three places: asynchrony, mutation, and the gap between what the runtime does and what the code appears to say. ## Async ### 1. Floating promises ```js async function save(user) { db.write(user); // not awaited. errors vanish, ordering is undefined. return { ok: true }; } ``` The most common real bug in generated JavaScript. The function returns before the write happens, the error becomes an unhandled rejection, and in Node 15+ an unhandled rejection terminates the process. **Catch it with:** `@typescript-eslint/no-floating-promises`. This rule alone justifies running TypeScript-aware linting even on a JavaScript codebase (via JSDoc types or `checkJs`). ### 2. `await` in a loop where it should be parallel ```js for (const id of ids) { results.push(await fetchUser(id)); // 100 sequential round trips } ``` Correct, and 100x slower than it needs to be. Generated code reaches for the loop because it reads more clearly. ```js const results = await Promise.all(ids.map(fetchUser)); // with a concurrency limit, which you usually want const results = []; for (const batch of chunk(ids, 10)) { results.push(...await Promise.all(batch.map(fetchUser))); } ``` **Catch it with:** `no-await-in-loop`, then whitelist the cases where sequencing is deliberate. ### 3. `Promise.all` where you meant `allSettled` `Promise.all` rejects on the first failure and abandons the rest. For "fetch these ten things, show me what you got", that is wrong. `Promise.allSettled` is almost always what generated batch code should have used. ### 4. `forEach` with an async callback ```js items.forEach(async (item) => { await process(item); // forEach ignores the returned promise }); console.log("done"); // prints immediately. nothing is done. ``` Looks like it awaits. Does not. **Catch it with:** `no-misused-promises`. ### 5. Race conditions in React effects ```jsx useEffect(() => { fetch(`/api/user/${id}`) .then(r => r.json()) .then(setUser); // a stale response can arrive after a newer one }, [id]); ``` Change `id` twice quickly and the slower first request can resolve last, leaving the wrong user on screen. Generated effect code omits the cleanup almost every time. ```jsx useEffect(() => { const ac = new AbortController(); fetch(`/api/user/${id}`, { signal: ac.signal }) .then(r => r.json()) .then(setUser) .catch(e => { if (e.name !== "AbortError") throw e; }); return () => ac.abort(); }, [id]); ``` ### 6. `try/catch` that does not catch ```js try { setTimeout(() => { throw new Error("boom"); }, 0); // uncatchable here } catch (e) { /* never runs */ } ``` The same applies to any callback invoked later. Errors only propagate up the call stack that exists when they are thrown. ## Mutation and equality ### 7. Mutating props, state or arguments ```js function addTax(order) { order.total *= 1.2; // mutates the caller's object return order; } ``` Generated code mutates freely because most JavaScript in the training data does. In React it produces components that do not re-render; elsewhere it produces action at a distance. **Catch it with:** `no-param-reassign` with `props: true`, and `Object.freeze` on shared config in development. ### 8. Shallow copy assumed to be deep ```js const copy = { ...original }; // nested objects are still shared copy.address.city = "Paris"; // original.address.city is now Paris ``` **Correct:** `structuredClone(original)` — built in, no library needed. ### 9. `==` and truthiness on values that can be `0` or `""` ```js if (!count) { /* also true when count is 0 */ } const name = input.name || "anonymous"; // "" becomes "anonymous" ``` **Correct:** `??` for nullish defaults, explicit `=== undefined` checks. `eqeqeq` in ESLint. ### 10. `sort()` on numbers ```js [10, 9, 1].sort() // [1, 10, 9] — it sorts as strings ``` Still generated. Also: `sort` mutates in place. Use `toSorted((a, b) => a - b)` in modern runtimes. ## Stale idioms Not bugs, but a reliable signal that the generated code came from older training data — and worth correcting in `AGENTS.md` because they accumulate. | Generated | Current | |---|---| | `var` | `const` / `let` | | `require()` in an ESM project | `import` | | `moment` | `Temporal`, or `Intl.DateTimeFormat` | | `lodash.get` | optional chaining `?.` | | `axios` for one request | `fetch` (stable in Node since 18) | | `new Promise` wrapping a callback | `util.promisify`, or the promise API that already exists | | `Array.prototype.indexOf(x) !== -1` | `.includes(x)` | | `JSON.parse(JSON.stringify(x))` | `structuredClone(x)` | | `__dirname` in ESM | `import.meta.dirname` | ## Node specifics ### 11. Unhandled rejection kills the process Since Node 15 the default is to terminate. Generated background jobs and event handlers frequently have no `.catch`, which turns a transient failure into a restart. ### 12. Blocking the event loop `fs.readFileSync`, `JSON.parse` on a large payload, `crypto.pbkdf2Sync`, a synchronous loop over a big array in a request handler. One request blocks all of them. ### 13. Path traversal in file handlers ```js app.get("/files/:name", (req, res) => { res.sendFile(path.join(DIR, req.params.name)); // ../../etc/passwd }); ``` **Correct:** resolve, then verify the result is still inside `DIR`. ## The config that catches most of it ```js eslint.config.js import js from "@eslint/js"; import ts from "typescript-eslint"; export default [ js.configs.recommended, ...ts.configs.recommendedTypeChecked, // needs type information { languageOptions: { parserOptions: { projectService: true } }, rules: { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/await-thenable": "error", "no-await-in-loop": "warn", "require-atomic-updates": "error", "no-param-reassign": ["error", { props: true }], "eqeqeq": ["error", "smart"], }, }, ]; ``` The type-checked rules are the important part. `no-floating-promises` cannot work without type information, and it is the highest-value rule in the list. On a plain JavaScript project, enable `checkJs` in `jsconfig.json` to get it. :::verdict The short version Turn on type-aware ESLint and the async rules. Most of the top half of this page becomes a build failure, and what is left — effect races, blocking the loop, path traversal — is a short enough list to hold in your head while reviewing. ::: :::promo frontendmasters ::: ## Common questions ### Do I need TypeScript to get these lint rules? No, but you need type information. Adding `jsconfig.json` with `checkJs: true` gives ESLint enough to run the type-aware rules on plain JavaScript. It is a much smaller step than a TypeScript migration and it unlocks the rule that matters most. ### Why is so much generated JavaScript written in an old style? Because the training data is dominated by a decade of pre-2020 JavaScript, and the language changed a great deal in that time. `var`, `moment`, callback wrapping and `axios` are all overwhelmingly represented. It is rarely harmful, but it accumulates, and a few lines in `AGENTS.md` correct it. ### Is `no-await-in-loop` too aggressive? As an error, yes — sequential awaits are correct when each iteration depends on the last, or when you are deliberately rate-limiting. As a warning it is well calibrated: it makes you justify the sequencing, which is exactly the question worth asking. ## Variables and Types Source: https://learn-javascript.org/variables-and-types/ Like almost every dynamic language, JavaScript is a “duck-typed” language, and therefore every variable is defined using the `var` keyword, and can contain all types of variables. We can define several types of variables to use in our code: ```javascript let myNumber = 3; // a number let myString = "Hello, World!" // a string let myBoolean = true; // a boolean ``` A few notes about variable types in JavaScript: - In JavaScript, the Number type can be both a floating point number and an integer. - Boolean variables can only be equal to either `true` or `false`. There are two more advanced types in JavaScript. An array, and an object. We will get to them in more advanced tutorials. ```javascript let myArray = []; // an array let myObject = {}; // an object ``` On top of that, there are two special types called `undefined` and `null`. When a variable is used without first defining a value for it, it is equal to undefined. For example: ```javascript let newVariable; console.log(newVariable); //prints undefined ``` However, the `null` value is a different type of value, and is used when a variable should be marked as empty. `undefined` can be used for this purpose, but it should not be used. ```javascript let emptyVariable = null; console.log(emptyVariable); ``` will print out `null` ## Arrays Source: https://learn-javascript.org/arrays/ JavaScript can hold an array of variables in an Array object. In JavaScript, an array also functions as a list, a stack or a queue. To define an array, either use the brackets notation or the Array object notation: ```javascript let myArray = [1, 2, 3]; let theSameArray = new Array(1, 2, 3); ``` ### Addressing We can use the brackets `[]` operator to address a specific cell in our array. Addressing uses zero-based indices, so for example, in `myArray` the 2nd member can be addressed with index 1. One of the benefits of using an array datastructure is that you have constant time look-up, if you already know the index of the element you are trying to access. ```javascript console.log(myArray[1]); // prints out 2 ``` Arrays in JavaScript are sparse, meaning that we can also assign variables to random locations even though previous cells were undefined. For example: ```javascript let myArray = [] myArray[3] = "hello" console.log(myArray); ``` Will print out: ```javascript [undefined, undefined, undefined, "hello"] ``` ### Array Elements Because JavaScript Arrays are just special kinds of objects, you can have elements of different types stored together in the same array. The example below is an array with a string, a number, and an empty object. ```javascript let myArray = ["string", 10, {}] ``` ## npm dependency hygiene: install scripts, slopsquatting and transitive bloat Source: https://learn-javascript.org/review/dependencies/ npm has two properties that make it the highest-risk dependency ecosystem here: **installing executes code**, and the dependency graphs are enormous. A single `npm install` can run hundreds of lifecycle scripts from packages you have never heard of. Layer generated code on top — which suggests package names it has inferred rather than verified — and the review discipline matters more than in any other language on this network. ## Hallucinated packages, and why npm is the worst place for it Ask a model for a library that does something slightly unusual and it will sometimes give you a name that sounds exactly right and does not exist. Attackers register the ones that recur. The industry name is **slopsquatting**, and npm is the most attractive target because installation runs code. ```bash npm i express-rate-limiter-redis # plausible. may not be the package you think. ``` Three shapes to recognise: - **Niche requirement.** Sparse training data, high invention rate. - **Renamed or absorbed package.** `request` (deprecated), `node-fetch` (now redundant), `faker` (forked to `@faker-js/faker`). The old name may now be owned by someone else. - **Wrong ecosystem.** A real PyPI or crates.io name suggested as an npm one. ### The four-second check ```bash npm view # exists? who publishes it? when was it first released? npm view time.created dist-tags maintainers repository.url ``` Then ask three questions on the npm page: 1. **First published when?** A package solving an old problem that appeared last month is a red flag. 2. **Weekly downloads?** Three downloads for a "popular utility" is a red flag. 3. **Does the repository link resolve**, with history and issues from other people? :::danger The install command is the dangerous moment The risk is not the model suggesting a name — it is you pasting an install command without reading it. Put `Bash(npm install:*)` and `Bash(pnpm add:*)` in the **ask** list of your [permission config](/ai/agent-setup/), never the allow list. That single line turns this from a real risk into a non-issue. ::: ## Turn install scripts off This is the highest-value npm-specific control and almost nobody uses it. ```bash npm config set ignore-scripts true # or per project, in .npmrc echo "ignore-scripts=true" >> .npmrc ``` Now `postinstall` hooks do not run. A handful of packages genuinely need them — native modules that compile, some binary downloaders — and you allow those explicitly: ```bash npm rebuild sharp # opt in, deliberately, for the ones that need it ``` pnpm goes further: recent versions block lifecycle scripts by default and require you to list the packages allowed to run them: ```yaml pnpm-workspace.yaml onlyBuiltDependencies: - esbuild - sharp ``` That allowlist is a genuinely good design — it makes "which of my 900 dependencies can execute code at install time" an explicit, reviewable list of three. ## Lockfiles and CI ```bash npm ci # installs exactly the lockfile. fails if package.json disagrees. npm install # may update the lockfile. never in CI. ``` Using `npm install` in CI means your build can silently pick up a different version than you tested. `npm ci` (or `pnpm install --frozen-lockfile`) is the only correct command there. **Read the lockfile diff.** A one-line `package.json` change is often a two-hundred-line lockfile change, and that diff is the transitive dependencies you just accepted — the only place you will ever see them. ```bash npm i some-lib git diff --stat package-lock.json # how many new packages did that pull in? ``` ## The transitive problem ```bash npm ls --all | wc -l # how many packages are actually installed? npx howfat some-lib # size and dependency count before installing ``` A utility that pulls in forty transitive packages for one function is a decision, not an accident. Each one is a maintainer who could be compromised, an account that could be taken over, and a package that will eventually be unmaintained. The most effective reduction is knowing what the platform now does. Generated JavaScript reaches for the 2018 dependency because that is what dominates the training data: | Generated reaches for | Platform now has | |---|---| | `axios`, `node-fetch`, `request` | global `fetch` | | `uuid` | `crypto.randomUUID()` | | `dotenv` | `node --env-file=.env` | | `moment`, most `date-fns` use | `Intl.DateTimeFormat`, `Temporal` | | `lodash` | `Object.groupBy`, `toSorted`, `at`, `?.`, `??`, `structuredClone` | | `rimraf`, `mkdirp` | `fs.rm` / `fs.mkdir` with `recursive` | | `chalk` (simple cases) | `util.styleText` | | `glob` (simple cases) | `fs.glob` | | `qs` | `URLSearchParams` | | `body-parser` | `express.json()`, built in since Express 4.16 | Putting that table in [your `AGENTS.md`](/ai/agents-md/) stops the drift at source, which is much easier than removing dependencies later. ## Pin, audit, and update continuously ```bash npm audit --omit=dev # production tree only — dev noise is not your risk npm audit fix # careful: can change majors npx depcheck # what is installed and never imported? ``` `npm audit` is noisier than Go's `govulncheck` because it has no reachability analysis — it reports every advisory in the tree, including ones in dev-only tooling you never ship. `--omit=dev` cuts most of the noise. The failure mode to avoid is a team that mutes it entirely because it cries wolf. Enable Dependabot or Renovate. The individual updates rarely matter; what matters is that a repository receiving updates continuously is one where a security update can be merged in an afternoon rather than being a project. ## Provenance and pinning ```bash npm view dist.attestations # was it published from CI with provenance? ``` npm provenance links a published package to the source commit and workflow that built it. Preferring packages that publish with provenance is a real signal, and it is free to check. For high-value dependencies, consider pinning exact versions rather than ranges, and reviewing updates deliberately: ```json "dependencies": { "some-critical-lib": "1.4.2" } ``` Ranges plus a lockfile are fine for most things. Exact pins are worth it for anything in an auth or payment path. :::verdict The whole policy, in six lines 1. Package installs go behind a confirmation, never on the agent's allowlist. 2. `ignore-scripts=true`, with an explicit allowlist for the few that need it. 3. Unfamiliar name? Check first-published date, downloads, repository. 4. `npm ci` in CI. Never `npm install`. 5. Read the lockfile diff. 6. `npm audit --omit=dev` in CI, plus Renovate or Dependabot. ::: ## Common questions ### Is `npm audit` worth running given the noise? With `--omit=dev`, yes. Most of the noise comes from advisories in build tooling that never reaches production, and filtering to the production tree makes the output small enough that people act on it. A muted scanner is worth nothing. ### npm, pnpm or yarn? pnpm, for two reasons that are both security-relevant: its strict `node_modules` layout prevents phantom dependencies (using a package you never declared), and recent versions block lifecycle scripts by default with an explicit allowlist. npm is fine and universal; the pnpm defaults are simply safer. ### How risky are install scripts really? They are the mechanism behind most real npm supply-chain incidents, because they run automatically, with your user's permissions, before you have reviewed anything. `ignore-scripts=true` costs you an occasional `npm rebuild` and removes the entire vector. ### Should I vendor dependencies? Rarely worth it for npm — the lockfile plus a registry cache gives you reproducibility, and vendoring `node_modules` produces unreviewable diffs. If you need availability guarantees, run a registry proxy instead. ## Writing an AGENTS.md for JavaScript Source: https://learn-javascript.org/ai/agents-md/ `AGENTS.md` is a Markdown file in your repository root that coding agents read before they start. Claude Code reads `CLAUDE.md`; most other tools read `AGENTS.md`. Write one and symlink the other: ```bash ln -s AGENTS.md CLAUDE.md ``` JavaScript is the language where this file does the most work, for two reasons. There is no compiler rejecting anything, and the training data spans fifteen years of a language that changed enormously in that time. So the model's defaults are drawn from an average of every JavaScript era at once, and your file is what pulls it to the current one. ## The section other languages do not need The highest-value block in a JavaScript `AGENTS.md` is a list of things the platform now does that a package used to do. Generated code reaches for the 2018 dependency by default — not because it is wrong, but because there is vastly more of it in the training data — and those dependencies accumulate silently until you have a `package.json` full of things Node ships natively. ```markdown ## Use the platform, not a package fetch not axios / node-fetch crypto.randomUUID() not uuid node --env-file not dotenv structuredClone not JSON.parse(JSON.stringify(x)) fs.rm / fs.mkdir with { recursive: true } not rimraf / mkdirp Intl.DateTimeFormat, Temporal not moment Object.groupBy, Array.at, toSorted, ?., ?? not lodash util.styleText not chalk (for simple cases) fs.glob not glob (for simple cases) import.meta.dirname not __dirname shims ``` Ten lines, and they prevent a category of drift that is otherwise invisible until a dependency audit. Every one of those is a real supply-chain surface you did not need. ## What else belongs **Commands.** Unguessable, used every turn. **The async rules.** This is where [generated JavaScript actually breaks](/review/failure-modes/), and unlike the platform list above, some of it is enforceable — so enforce what you can and state the rest. **Module system and Node version.** ESM or CJS, and which Node. Generated code mixes `require` and `import` in the same file if you do not say. **Boundaries.** Which directory imports which. **Landmines.** :::verdict The target **Under 100 lines**, of which about fifteen are the platform list. Everything enforceable should be a lint rule instead — see below. ::: ## Enforce first, then write Even without TypeScript you can get type-aware linting, and it catches the bugs that matter most here. ```json jsconfig.json { "compilerOptions": { "checkJs": true, "strict": true, "noEmit": true }, "include": ["src/**/*.js"] } ``` ```js eslint.config.js import js from "@eslint/js"; import ts from "typescript-eslint"; export default [ js.configs.recommended, ...ts.configs.recommendedTypeChecked, { languageOptions: { parserOptions: { projectService: true } }, rules: { "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", "@typescript-eslint/await-thenable": "error", "require-atomic-updates": "error", "no-param-reassign": ["error", { props: true }], "no-await-in-loop": "warn", "eqeqeq": ["error", "smart"], }, }, ]; ``` `no-floating-promises` alone removes the most common bug in generated JavaScript, and it is worth adding `jsconfig.json` purely to unlock it. Three lines of your instructions file become mandatory instead of advisory. ## The template ```markdown AGENTS.md Node 22, ESM only (package.json has "type": "module"), pnpm. jsconfig.json has checkJs: true — keep `pnpm typecheck` passing. ## Commands - Check: `pnpm check` (tsc --noEmit, eslint, vitest run). Must pass. - Test: `pnpm vitest run` - One test: `pnpm vitest run src/thing.test.js` - Fix: `pnpm eslint . --fix` ## Layout - `src/core/` pure logic. No fetch, no fs, no process.env. - `src/adapters/` everything touching the outside world. - `src/routes/` http handlers. Parse, call core, respond. - Tests beside the file: `thing.js` -> `thing.test.js`. ## Async (this is where things actually break) - await every promise. No floating promises. - Promise.all for independent work. A sequential await loop needs a comment saying why the sequencing is required. - Promise.allSettled when partial success is acceptable. - Never pass an async function to forEach — it does not await. - Every fetch in a React effect aborts on cleanup (AbortController). - Nothing synchronous and slow in a request handler: no readFileSync, no large JSON.parse, no crypto *Sync. ## Values - `??` for defaults, never `||` — 0 and "" are valid values. - `===` always. - Do not mutate arguments, props or state. structuredClone for deep copies. - Sort numbers with a comparator: toSorted((a, b) => a - b). ## Use the platform, not a package fetch not axios · crypto.randomUUID() not uuid · node --env-file not dotenv structuredClone not JSON round-tripping · fs.rm not rimraf Intl/Temporal not moment · Object.groupBy / toSorted / ?. / ?? not lodash New dependencies need a sentence in the PR description justifying them. ## Landmines - `src/legacy/` is CommonJS and is loaded by the worker process. Do not convert it to ESM. - `src/routes/webhook.js` verifies a signature over the RAW body. Do not add body parsing middleware in front of it. ``` That webhook line is a good example of what a landmine looks like: invisible in the code, catastrophic if changed, and something a model would "helpfully" break while tidying middleware. :::tip The two-strike rule Do not add a rule speculatively. Wait until you have corrected the same thing twice. It keeps the file short and tells you which of your conventions are genuinely non-obvious. ::: ## Common questions ### Should I just migrate to TypeScript instead? If the codebase is small or new, probably. If it is large, `checkJs` plus JSDoc types gets you the type-aware lint rules and most of the editor benefit without a migration project — and you can convert file by file later if it proves worth it. ### Why does generated JavaScript keep reaching for old packages? Because a decade of JavaScript in the training data uses them, and the replacements are recent. It is rarely harmful in itself; the cost is accumulation — every unnecessary dependency is a supply-chain surface and a future upgrade. The platform list is a cheap fix. ### CommonJS or ESM? ESM for anything new, and say so explicitly in the file, because generated code will otherwise mix the two. If you have a CJS section that must stay, name it as a landmine rather than hoping the model infers the boundary. ## Manipulating Arrays Source: https://learn-javascript.org/manipulating-arrays/ ### Pushing and popping Arrays can also function as a stack. The `push` and `pop` methods insert and remove variables from the end of an array. For example, let’s create an empty array and push a few variables. ```javascript let myStack = []; myStack.push(1); myStack.push(2); myStack.push(3); console.log(myStack); ``` This will print out: ```javascript 1,2,3 ``` After pushing variables to the array, we can then pop variables off from the end. ```javascript console.log(myStack.pop()); console.log(myStack); ``` This will print out the variable we popped from the array, and what’s left of the array: ```javascript 3 // the result from myStack.pop() 1,2 // what myStack contains now ``` ### Queues using shifting and unshifting The `unshift` and `shift` methods are similar to `push` and `pop`, only they work from the beginning of the array. We can use the `push` and `shift` methods consecutively to utilize an array as a queue. For example: ```javascript let myQueue = []; myQueue.push(1); myQueue.push(2); myQueue.push(3); console.log(myQueue.shift()); console.log(myQueue.shift()); console.log(myQueue.shift()); ``` The `shift` keyword will remove the variables of the array in the exact order they were inserted in, and the output will be: ```javascript 1 2 3 ``` The `unshift` method is used to insert a variable at the beginning of an array. For example: ```javascript let myArray = [1,2,3]; myArray.unshift(0); console.log(myArray); // will print out 0,1,2,3 ``` ### Splicing Splicing arrays in JavaScript removes a certain part from an array to create a new array, made up from the part we took out. For example, if we wanted to remove the five numbers from the following array beginning from the 3rd index, we would do the following: ```javascript let myArray = [0,1,2,3,4,5,6,7,8,9]; let splice = myArray.splice(3,5); console.log(splice); // will print out 3,4,5,6,7 console.log(myArray); // will print out 0,1,2,8,9 ``` After splicing the array, it will only contain the part before and after the splicing. The splice is equal to all the variables between 3 and 7 (inclusive), and the remainder of the array, which contains all variables between 0 and 2 (inclusive), and 8 to 9 (inclusive). ## Security review checklist for AI-generated JavaScript Source: https://learn-javascript.org/review/security/ Generated JavaScript is usually better than the average tutorial it learned from — it parameterises queries, it hashes passwords properly. What survives is a specific set of failures rooted in the language's dynamism and in Node's defaults. ## Turn on the machine checks first ```bash npm i -D eslint eslint-plugin-security @microsoft/eslint-plugin-sdl npm audit --omit=dev ``` Type-aware ESLint is the bigger win. `no-floating-promises` is a correctness rule but it is also a security one: an unhandled rejection in an auth path can leave a request half-processed. See [the failure-mode catalogue](/review/failure-modes/) for the config. ## The language-specific one: prototype pollution JavaScript's inheritance model creates a vulnerability class that does not exist elsewhere. ```js function merge(target, source) { for (const key in source) { if (typeof source[key] === "object") merge(target[key] ??= {}, source[key]); else target[key] = source[key]; } return target; } merge({}, JSON.parse('{"__proto__":{"isAdmin":true}}')); ({}).isAdmin; // true — every object in the process now has it ``` Generated deep-merge, config-loading and query-parsing helpers produce this constantly, because the naive recursive merge is what appears everywhere. ```js const BLOCKED = new Set(["__proto__", "constructor", "prototype"]); function merge(target, source) { for (const key of Object.keys(source)) { if (BLOCKED.has(key)) continue; // the whole fix if (source[key] && typeof source[key] === "object") { merge((target[key] ??= Object.create(null)), source[key]); } else { target[key] = source[key]; } } return target; } ``` Three defences worth knowing: skip the dangerous keys, use `Object.create(null)` for anything holding untrusted keys, and prefer `Map` over a plain object for user-controlled lookups. `Object.freeze(Object.prototype)` at startup is a blunt but effective backstop. **Catch it with:** `eslint-plugin-security`'s `detect-object-injection` (noisy but finds it), and a grep for `for (const k in` in merge-like functions. ## Injection ### Code execution ```js eval(userInput); new Function(`return ${expr}`)(); setTimeout("doThing()", 100); // string form is eval vm.runInNewContext(code); // NOT a security boundary ``` `vm` is the trap people get wrong: it is for isolation of *trusted* code, not a sandbox. Untrusted code needs a separate process with `isolated-vm` or a real container. ### Command execution ```js exec(`git log --author=${author}`); // shell, injectable execFile("git", ["log", `--author=${author}`]); // no shell ``` Node's `exec` runs through a shell; `execFile` and `spawn` do not. Generated code reaches for `exec` because template literals read nicely. ### SQL and NoSQL ```js db.query(`SELECT * FROM users WHERE email = '${email}'`); // parameterise ``` And the Mongo-specific one, which generated code produces routinely: ```js User.findOne({ email: req.body.email, password: req.body.password }); // client sends {"password": {"$gt": ""}} and matches any user ``` Validate that query values are primitives before they reach the driver, or use a schema at the boundary. ## XSS ```jsx
// stored XSS element.innerHTML = userContent; document.write(location.hash); ``` React escapes by default, which is why the *only* XSS in most React apps is `dangerouslySetInnerHTML`, `href={userUrl}` (a `javascript:` URL), and `ref` callbacks writing raw HTML. ```js // if you must render user HTML import DOMPurify from "dompurify"; el.innerHTML = DOMPurify.sanitize(html); // and validate URLs const u = new URL(userUrl, base); if (!["http:", "https:"].includes(u.protocol)) throw new Error("bad scheme"); ``` **Catch it with:** `react/no-danger`, and `grep -rn 'innerHTML\|dangerouslySetInnerHTML\|document.write'`. ## ReDoS A generated regex with nested quantifiers, applied to input you do not control, blocks the entire event loop — so in Node a single request takes down the process for every user. ```js /^(\s*\w+)*$/.test(input) // catastrophic backtracking /^(a+)+$/.test(input) ``` This is worse in Node than in a threaded runtime: one bad request means total unavailability, not one slow response. **Correct:** flatten the pattern, anchor it, bound the input length before matching, and prefer a parser for anything structured. **Catch it with:** `eslint-plugin-security`'s `detect-unsafe-regex`, and `redos-detector` in CI for generated patterns. ## Node-specific ### Path traversal ```js app.get("/files/:name", (req, res) => { res.sendFile(path.join(DIR, req.params.name)); // ../../etc/passwd }); ``` ```js const full = path.resolve(DIR, req.params.name); if (!full.startsWith(DIR + path.sep)) return res.sendStatus(403); ``` The `+ path.sep` matters — without it `/var/data-evil` passes a prefix check against `/var/data`. ### SSRF ```js const r = await fetch(req.query.url); // fetches your cloud metadata endpoint ``` Allowlist scheme and host, resolve the hostname and reject private ranges, and disable redirects — an attacker controls a public host that redirects to `169.254.169.254`. ```js const r = await fetch(url, { redirect: "manual", signal: AbortSignal.timeout(5000) }); ``` ### Unbounded input ```js app.use(express.json()); // default limit is small but check it const body = await new Response(req).text(); // no limit at all ``` Set an explicit body size limit, and use `AbortSignal.timeout()` on every outbound request. An unbounded `JSON.parse` on a large payload is also an event-loop block, so this is a denial-of-service issue as well as a memory one. ## Auth and secrets ### JWT handling ```js jwt.verify(token, secret); // fine jwt.decode(token); // does NOT verify. ever. jwt.verify(token, secret, { algorithms: undefined }); // allows "none" in old libs ``` Always pass an explicit `algorithms` allowlist. And storing a JWT in `localStorage` makes it readable by any XSS — an httpOnly, Secure, SameSite cookie is the safer default. ### Timing-unsafe comparison ```js if (token === expected) { } // early return leaks length and prefix crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); // requires equal lengths ``` ### Secrets in logs ```js logger.info("calling %s", url, { headers }); // headers include Authorization console.log("user", user); // user includes passwordHash ``` The second one is the common case: an object logged wholesale that happens to carry a secret field. Redact at the logger, not at each call site. ## Dependencies npm's install scripts make this the largest supply-chain surface of any ecosystem — a compromised transitive package runs code on your machine at install time. That has its own page: [dependency hygiene](/review/dependencies/). ## The review, as commands ```bash git diff | grep -nE "eval\(|new Function|innerHTML|dangerouslySetInnerHTML|document\.write" git diff | grep -nE "\bexec\(|child_process|vm\.run" git diff | grep -niE "api_key|secret|token|password|authorization" git diff | grep -nE "jwt\.decode|localStorage\.setItem\(.token|verify=false" npx eslint . && npm audit --omit=dev ``` Thirty seconds. What is left for human attention is the two things no tool finds: **missing authorisation checks** and **SSRF**, because both need knowing what your system is meant to allow. Authorisation is worth the one test per resource type: ```js test("cannot read another user's invoice", async () => { const res = await request(app).get(`/invoices/${bobsInvoice.id}`).set(aliceAuth); expect(res.status).toBe(404); // 404, not 403 — do not confirm it exists }); ``` :::verdict The JavaScript-specific ones to remember **Prototype pollution** in any merge or deep-assign helper, and **ReDoS** in any generated regex applied to untrusted input. Both are language-specific, both are routinely generated, and the second one takes your whole process down rather than one request. ::: ## Common questions ### Is prototype pollution still a real risk? Yes. Modern frameworks have hardened their own paths, but the vulnerability lives in application code — the deep-merge helper, the config loader, the query-string parser someone wrote. Generated code produces the vulnerable pattern because the naive recursive merge is what dominates the training data. ### Does TypeScript prevent any of this? Some of it — typed boundaries make it harder to pass an unexpected shape — but types are erased at runtime, so an attacker sending `__proto__` is unaffected by your type annotations. See [the TypeScript security page](https://learn-typescript.org/review/security/) for what types do and do not buy you. ### Why is ReDoS worse in Node than elsewhere? Because Node is single-threaded. A catastrophic regex in a threaded server makes one request slow; in Node it blocks the event loop, so every concurrent user is affected until it finishes. That turns a performance bug into total unavailability. ### What is the highest-value check for a web app? The authorisation test above, one per resource-returning endpoint. No linter finds a missing ownership check, it is the most common real vulnerability in generated web code, and the test is a single line. ## Testing JavaScript that calls a language model Source: https://learn-javascript.org/ai/evals/ More and more Node code is a thin layer around a model call. That code has properties ordinary JavaScript does not: the same input gives different output, each test costs money and a second or two, and — the part specific to JavaScript — **most of it streams**, which means half your bug surface is in the plumbing rather than the model. The instinct is to mock the SDK and test nothing meaningful. There is a better middle, and it looks like a pyramid. ## The pyramid ```text few, slow, expensive ┌──────────────────────────┐ │ 4. live evals (nightly) │ real model, scored dataset ├──────────────────────────┤ │ 3. cassette tests (CI) │ recorded responses, real shapes ├──────────────────────────┤ │ 2. contract tests │ parsing, streaming, aborts, retries ├──────────────────────────┤ │ 1. pure logic tests │ prompt building, chunking, routing └──────────────────────────┘ many, fast, free ``` Most codebases have only layer 4, run it rarely because it is expensive, and therefore have no signal at all while developing. Invert that. ## Layer 0 — the seam that makes everything else possible Before any of this works you need one narrow interface between your code and the provider. Everything above it becomes testable without a network. ```js src/llm/client.js /** * @typedef {{ system: string, messages: Message[], signal?: AbortSignal }} Request * @typedef {{ complete(req: Request): Promise, * stream(req: Request): AsyncIterable }} LLM */ /** @returns {LLM} */ export function makeClient({ apiKey, model, fetchImpl = fetch }) { /* … */ } ``` Two methods. Everything in your application depends on that shape, not on an SDK. Swapping in a fake is then one line, and it does not break when the SDK's internals change. ## Layer 1 — most of your code is not the model call Prompt assembly, chunking, retrieval ranking, routing, token accounting, output post-processing. All ordinary JavaScript, all testable in milliseconds. ```js src/prompt.test.js import { expect, test } from "vitest"; import { buildPrompt } from "./prompt.js"; test("includes only the top k chunks", () => { const p = buildPrompt({ query: "q", chunks: makeChunks(20), k: 5 }); expect(p.match(//g)).toHaveLength(5); }); test("truncates a chunk that would blow the budget", () => { const p = buildPrompt({ query: "q", chunks: [huge()], k: 1, maxChars: 1000 }); expect(p.length).toBeLessThanOrEqual(1000); }); ``` Unglamorous, and it catches a surprising share of real bugs. ## Layer 2 — contract tests against a fake Test what your code does with a response, not what the model says. Every case below is a real production failure: ```js src/parse.test.js import { describe, expect, test } from "vitest"; import { parseCategory } from "./parse.js"; describe("parseCategory", () => { test.each([ ['{"category":"billing"}', "billing"], ['```json\n{"category":"billing"}\n```', "billing"], // models fence constantly ['{"category":"Billing"}', "billing"], // wrong case ['Sure! Here you go:\n{"category":"billing"}', "billing"], ['{"categorie":"billing"}', null], // misspelled key ['{"category":"refunds_and_returns"}', null], // not in the enum ["I think this is a billing issue.", null], // ignored the format ["", null], ['{"category":"billing"', null], // truncated at max tokens ])("handles %j", (raw, expected) => { expect(parseCategory(raw)).toBe(expected); }); }); ``` The contract is **never throw, never invent**. A parser that returns a plausible wrong answer on malformed input is worse than one returning `null`. ### The tests JavaScript needs that Python does not Streaming is where the JavaScript-specific bugs live. Fake the stream and test the plumbing: ```js src/stream.test.js async function* fakeStream(chunks, { failAt = -1 } = {}) { for (const [i, c] of chunks.entries()) { if (i === failAt) throw new Error("connection reset"); yield c; } } test("assembles chunks that split a multi-byte character", async () => { const out = await collect(fakeStream(["hello ", "wor", "ld 👋"])); expect(out).toBe("hello world 👋"); }); test("a mid-stream failure surfaces, and does not leave a half-written record", async () => { const db = makeFakeDb(); await expect(persistAnswer(db, fakeStream(["a", "b"], { failAt: 1 }))) .rejects.toThrow("connection reset"); expect(db.rows).toHaveLength(0); // no partial write }); test("aborting stops consumption promptly", async () => { const ac = new AbortController(); const p = streamAnswer({ signal: ac.signal }); ac.abort(); await expect(p).rejects.toMatchObject({ name: "AbortError" }); }); test("a client disconnect aborts the upstream call", async () => { const upstream = new AbortController(); const spy = vi.fn(); upstream.signal.addEventListener("abort", spy); simulateClientDisconnect(); expect(spy).toHaveBeenCalled(); // otherwise you keep paying for tokens }); ``` That last one is the expensive bug. A user closes the tab, your server keeps streaming from the provider, and you are billed for output nobody will read. It is invisible until the bill arrives, and it is one test. Also worth covering here: retry with backoff on 429, timeout behaviour, and what happens on a 500 halfway through a stream. None of it needs a real model. :::tip Use structured output, and test the fallback anyway Constrained decoding and JSON-schema modes remove most parse failures and you should use them. Test the fallback path regardless: providers have outages, you will change models, and the day schema mode fails is the day you find out whether your parser was defensive. ::: ## Layer 3 — cassettes in CI Record real responses once, replay them forever. Real response shapes, zero cost, zero flakiness. Because everything goes through `fetch`, you can intercept at that level rather than at the SDK level — which means the cassette keeps working when you upgrade the SDK. ```js test/setup.js import { beforeAll, afterAll } from "vitest"; import { setupServer } from "msw/node"; import { http, HttpResponse } from "msw"; import { readFileSync } from "node:fs"; const cassette = JSON.parse(readFileSync("test/cassettes/classify.json", "utf8")); export const server = setupServer( http.post("https://api.example.com/v1/messages", async ({ request }) => { const body = await request.json(); const hit = cassette.find((c) => c.request.messages.at(-1).content === body.messages.at(-1).content); if (!hit) throw new Error(`No cassette entry. Re-record: npm run record`); return HttpResponse.json(hit.response); }), ); beforeAll(() => server.listen({ onUnhandledRequest: "error" })); afterAll(() => server.close()); ``` Two rules that stop this rotting: 1. **Scrub credentials before committing.** Read the first cassette you commit, in full. 2. **Re-record on a schedule**, monthly or so. A cassette from eighteen months ago tests a model that no longer exists, and the diff when you re-record is genuinely informative. ## Layer 4 — evals with a scored dataset Accept non-determinism and measure it instead of asserting on it. ```jsonl evals/dataset.jsonl {"input":"my card was charged twice","expect":"billing"} {"input":"how do I export my data?","expect":"support"} {"input":"cancel and refund please","expect":"refunds"} ``` ```js evals/run.js import { readFileSync } from "node:fs"; import { classify } from "../src/classify.js"; const cases = readFileSync("evals/dataset.jsonl", "utf8") .trim().split("\n").map((l) => JSON.parse(l)); const results = await Promise.all(cases.map((c) => classify(c.input))); const hits = results.map((r, i) => r.category === cases[i].expect); const accuracy = hits.filter(Boolean).length / hits.length; console.log(`accuracy ${(accuracy * 100).toFixed(1)}% on ${cases.length} cases`); for (const [i, ok] of hits.entries()) { if (!ok) console.log(` MISS ${JSON.stringify(cases[i].input)}: got ${results[i].category}, want ${cases[i].expect}`); } process.exit(accuracy >= 0.9 ? 0 : 1); ``` Nightly, not per-commit. A threshold, not an assertion. **Read the misses** — the list is worth more than the number, because it is where the next prompt change comes from. :::note Building the dataset is the actual work Fifty real, awkward examples beat five hundred synthetic ones. Take them from production logs and support tickets. Every time something goes wrong in production, the input becomes a case. That is the flywheel; the harness above is twenty lines. ::: ## When the output is free text No equality to assert. Three approaches, in decreasing order of how much you should trust them: **Assert on properties.** Deterministic, cheap, and catches most real regressions. ```js test("summary does not invent numbers", () => { const nums = (s) => new Set(s.match(/\d[\d,.]*/g) ?? []); expect([...nums(summary)].every((n) => nums(article).has(n))).toBe(true); }); ``` That one test catches fabricated figures, which is the failure that matters most in summarisation. **Assert on stability across runs.** Same input twice at temperature 0 should be similar. Large divergence is a signal even without ground truth. **LLM-as-judge, carefully.** A second model scores against a rubric. It works and has known biases — position, verbosity, self-preference. Use it for *relative* comparisons (is B better than A?) rather than absolute scores, and calibrate against fifty hand-labelled examples before you let a number gate anything. ## Cost control in CI ```yaml .github/workflows/test.yml on: [push, pull_request] jobs: fast: steps: - run: pnpm vitest run # layers 1-3. free, every push. evals: if: github.event_name == 'schedule' steps: - run: node evals/run.js # layer 4. nightly. ``` Set a hard spend cap on the CI key, not just an alert. Use the cheapest model that discriminates for any judge. ## Common questions ### Should I mock the SDK or `fetch`? `fetch`, or your own narrow client interface. Mocking the SDK's internals ties your tests to a library version and tests nothing you care about — and it breaks on every upgrade. Intercepting at the HTTP layer survives SDK changes and exercises your real request-building code. ### How do I test streaming without a real API? An async generator is a stream. Everything in layer 2 above uses one, and it lets you simulate the cases a real API rarely gives you on demand: a mid-stream failure, a chunk boundary that splits a multi-byte character, an abort. ### Temperature 0 makes it deterministic, so can I assert equality? No. Temperature 0 is greedy sampling, not determinism — batching, hardware and provider-side changes all move the output, and a model version change moves it substantially. Assert on properties, or use cassettes. ### What does running all this cost? Layers 1-3 are free — no tokens leave the process. Layer 4 is the billable one, and the abandoned-stream problem makes Node worse than most at wasting tokens; both are covered in [tracking and cutting token costs in Node](/ai/tokenomics/). ### Is `msw` necessary, or can I just stub global fetch? Stubbing `globalThis.fetch` works fine and has no dependency. `msw` is worth it once you have several endpoints or want the same mocks in browser tests. The important part is intercepting at the HTTP boundary either way. ## Operators Source: https://learn-javascript.org/operators/ Every variable in JavaScript is casted automatically so any operator between two variables will always give some kind of result. ### The addition operator The `+` (addition) operator is used for both addition and concatenation of strings. For example, adding two variables is easy: ```javascript let a = 1; let b = 2; let c = a + b; // c is now equal to 3 ``` The addition operator is used for concatenating strings to strings, strings to numbers, and numbers to strings: ```javascript let name = "John"; console.log("Hello " + name + "!"); console.log("The meaning of life is " + 42); console.log(42 + " is the meaning of life"); ``` JavaScript behaves differently when you are trying to combine two operands of different types. The default primitive value is a string, so when you try to add a number to a string, JavaScript will transform the number to a string before the concatenation. ```javascript console.log(1 + "1"); // outputs "11" ``` ### Mathematical operators To subtract, multiply and divide two numbers, use the minus (`-`), asterisk (`*`) and slash (`/`) signs. ```javascript console.log(3 - 5); // outputs -2 console.log(3 * 5); // outputs 15 console.log(3 / 5); // outputs 0.6 ``` ### Advanced mathematical operators JavaScript supports the modulus operator (`%`) which calculates the remainder of a division operation. ```javascript console.log(5 % 3); // outputs 2 ``` JavaScript also supports combined assignment and operation operators. So, instead of typing `myNumber = myNumber / 2`, you can type `myNumber /= 2`. Here is a list of all these operators: - `/=` - `*=` - `-=` - `+=` - `%=` JavaScript also has a `Math` module which contains more advanced functions: - `Math.abs` calculates the absolute value of a number - `Math.exp` calculates **e** to the power of a number - `Math.pow(x,y)` calculates the result of **x** to the power of **y** - `Math.floor` removes the fraction part from a number - `Math.random()` will give a random number `x` where 0 ## The performance traps in generated JavaScript Source: https://learn-javascript.org/review/performance/ JavaScript's performance story is different from every other language here because of one structural fact: **in Node, blocking is not slow — it is downtime.** One request that occupies the event loop for two seconds makes every concurrent user wait two seconds. Generated code is written for clarity and is correct. What it omits is the awareness that the runtime is single-threaded. :::note Measure it directly ```bash node --cpu-prof --cpu-prof-dir=./prof app.js # load it in Chrome DevTools node --inspect app.js # live profiling and heap snapshots npx clinic doctor -- node app.js # tells you which category you have ``` `clinic doctor` is the fastest first step: it categorises the problem as event loop, I/O, memory or CPU before you go looking. ::: ## Blocking the event loop ### 1. Synchronous I/O in a request path ```js app.get("/config", (req, res) => { const cfg = JSON.parse(fs.readFileSync("./config.json", "utf8")); // blocks everyone res.json(cfg); }); ``` Every `*Sync` function in a handler blocks the whole process. Common offenders: `readFileSync`, `existsSync`, `execSync`, `crypto.pbkdf2Sync`, `zlib.gunzipSync`. At startup, `*Sync` is fine and often clearer. In a request path it is never fine. **Catch it with:** `grep -rn 'Sync(' src/routes src/handlers` — a targeted grep beats a lint rule here, because the same functions are legitimate at module load. ### 2. Large `JSON.parse` and `JSON.stringify` Both are synchronous and both are O(n). A 50 MB payload parses for hundreds of milliseconds with everything else stopped. For large documents use a streaming parser; for large responses, stream the serialisation rather than building one string. ### 3. CPU-bound work in the main thread Image processing, hashing, compression, big sorts. Move it to a `worker_threads` pool, or out of the process entirely. ```js import { Worker } from "node:worker_threads"; // or, for a quick win, a pool: piscina ``` ### 4. Catastrophic regex A generated regex with nested quantifiers against untrusted input hangs the event loop. It appears under performance and under [security](/review/security/) for the same reason — in Node the two are the same failure. ## Async shapes ### 5. Sequential awaits that should be parallel ```js const user = await getUser(id); // three round trips, one after another const orders = await getOrders(id); const prefs = await getPrefs(id); ``` ```js const [user, orders, prefs] = await Promise.all([getUser(id), getOrders(id), getPrefs(id)]); ``` Correct either way; a third of the latency in the second. **Look for:** consecutive `await` lines with no data dependency between them. ### 6. `await` inside a loop ```js for (const id of ids) results.push(await fetchUser(id)); // 100 sequential trips ``` The fix is `Promise.all` over `.map` — but not unbounded, which is the next item. ### 7. Unbounded concurrency ```js await Promise.all(ids.map(fetchUser)); // 50,000 simultaneous requests ``` The overcorrection from item 6, and it is worse: file descriptor exhaustion, an immediate 429 storm, retries piling on top. ```js async function mapLimit(items, limit, fn) { const out = new Array(items.length); let i = 0; await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); } })); return out; } ``` Eight lines and no dependency. Generated fan-out code essentially never includes a limit. ### 8. `Promise.all` where `allSettled` was meant `Promise.all` rejects on the first failure and abandons the rest — but the work already started keeps running, so you pay for it and discard it. For "fetch these ten things and show me what you got", `allSettled`. ## Memory ### 9. Listeners never removed ```js emitter.on("data", handler); // in a function called per request ``` Each call adds a listener that is never removed, so the emitter's array grows forever and each captured closure keeps its scope alive. Node's `MaxListenersExceededWarning` is the tell — treat it as a leak, not as noise. The same applies to `setInterval` without `clearInterval`, and to `AbortController` listeners in long-lived objects. ### 10. Unbounded caches ```js const cache = new Map(); // grows until the process dies cache.set(key, value); ``` Generated caching code is a bare `Map` or object with no eviction. Use an LRU with a size bound, or `WeakMap`/`WeakRef` when the key's lifetime should govern. ### 11. Closures holding large objects ```js function makeHandler(hugeBuffer) { return () => doSomethingSmall(hugeBuffer.length); // retains the whole buffer } ``` The closure keeps everything in scope alive. Extract what you need before capturing. **Find leaks with:** two heap snapshots in DevTools, compare, sort by retained size. The dominant retainer is usually one of these three. ## Data access ### 12. N+1 queries ```js for (const order of orders) { order.items = await db.items.findMany({ where: { orderId: order.id } }); } ``` Correct, passes tests with three orders, 101 round trips in production. Fetch with `IN`/`ANY` and group in memory; with Prisma, `include`. **Catch it with:** a test asserting query count. It is the only reliable defence and the highest-value performance test in a service. ### 13. Array operations chained over large data ```js items.filter(f).map(m).filter(g).map(n) // four full passes, three intermediate arrays ``` Fine at a thousand items, meaningful at a million. One `reduce` or a plain loop for hot paths — but do not do this pre-emptively; the readable version is right until profiling says otherwise. ### 14. Repeated `array.includes` in a loop O(n) inside O(n). `new Set(array)` once, outside the loop. Same class as [the Python version](https://learn-python.com/review/performance/) and equally common. ## Startup and delivery ### 15. Everything imported at module load ```js import { hugeLibrary } from "huge-library"; // parsed and executed at startup ``` For a serverless function this is cold-start latency on every invocation. Dynamic `import()` for anything used on a minority of paths. ### 16. Bundle size in the browser A single unnecessary dependency can double a bundle. `npx source-map-explorer` or your bundler's analyser tells you where the weight is, and the [dependency table](/review/dependencies/) removes a surprising amount of it. ## The reviewer's shortcut Three questions on any diff: 1. **Is anything synchronous and slow in a request path?** (`*Sync`, big `JSON.parse`, a heavy regex) 2. **Is any `await` in a loop, and is any `Promise.all` unbounded?** 3. **Does anything add a listener, an interval, or a cache entry without a matching removal?** Those catch items 1–3, 5–7, and 9–11 — most of the real cost here. :::verdict The Node-specific mental model There is one CPU and everyone shares it. Any question of the form "is this slow?" is really "does this block, and for how long?" — and a 200 ms block under 100 concurrent requests is a 20-second tail. That framing catches more than any profiler. ::: ## Common questions ### How do I know if the event loop is blocked? `clinic doctor` tells you directly, and `perf_hooks`'s `monitorEventLoopDelay()` gives you a histogram you can put on a dashboard. Event-loop lag at p99 is one of the two or three most useful metrics a Node service can export. ### Are worker threads worth the complexity? For genuinely CPU-bound work that must stay in-process, yes — and use a pool rather than spawning per task, since worker startup is not free. For most services the better answer is moving that work out to a queue and a separate process entirely. ### Is `Promise.all` bad? No — it is right for a handful of independent operations. It becomes a problem when the array is user-sized rather than code-sized. The rule: if the length comes from data rather than from the source code, bound the concurrency. ### Should I optimise array chains pre-emptively? No. `filter().map()` is clearer and the difference is irrelevant below tens of thousands of elements. Profile first; the readable version is correct until measurement says otherwise. ## Tracking and cutting token costs in Node Source: https://learn-javascript.org/ai/tokenomics/ The economics are language-independent — [what tokens cost and where the money goes](https://codelearningdojo.com/token-economics/) is the model. This page is the Node implementation, and it leads with the failure that is specific to this ecosystem. ## The abandoned stream Node is where LLM output is streamed to a browser, and streaming introduces a cost bug that does not exist in a request/response system: **the consumer can disappear while the producer keeps going.** A user closes the tab. Your handler's response socket dies. If nothing propagates that upstream, your server keeps pulling tokens from the provider until the model stops talking — and you are billed for every one of them, for an answer that reached nobody. At any real traffic level this is a measurable share of the bill, and it is invisible: no error, no log line, no alert. ```js src/routes/chat.js export async function chat(req, res) { const ac = new AbortController(); // the whole fix: when the client goes, cancel upstream res.on("close", () => { if (!res.writableEnded) ac.abort(); }); let usage = null; try { const stream = await llm.stream({ messages: req.body.messages, signal: ac.signal }); for await (const event of stream) { if (event.type === "delta") res.write(event.text); if (event.type === "usage") usage = event.usage; } res.end(); } catch (err) { if (err.name !== "AbortError") throw err; } finally { record({ feature: "chat", usage, aborted: ac.signal.aborted }); } } ``` Three details: - **`res.on("close")`** fires on client disconnect *and* on normal completion, hence the `writableEnded` guard. - **`finally`** records usage even on abort. You still pay for the input tokens and whatever output was generated before the cancel, so an unrecorded abort understates your spend. - **Track the abort rate.** If it climbs, either your answers are too slow or too long. Both cost money. ```js // the metric worth having on a dashboard record({ feature, usage, aborted }); // then: aborted_calls / total_calls ``` :::warn Test this, because it fails silently There is a test for it in [testing JavaScript that calls a language model](/ai/evals/): assert that aborting the client's request fires abort on the upstream controller. Without that test you will not find out until you read a bill. ::: ## Attribution with AsyncLocalStorage Node's answer to "tag every call without threading a context object through fifteen functions". ```js src/llm/context.js import { AsyncLocalStorage } from "node:async_hooks"; export const als = new AsyncLocalStorage(); export function withRequestContext(handler) { return (req, res, next) => { als.run({ requestId: req.id, tenant: req.user?.tenantId, route: req.path }, () => handler(req, res, next)); }; } ``` ```js src/llm/record.js import { appendFile } from "node:fs/promises"; import { als } from "./context.js"; // per-1M-token prices. Config, not code — they change. const PRICES = { small: { in: 0.25, cachedIn: 0.03, out: 1.25 }, large: { in: 3.0, cachedIn: 0.3, out: 15.0 }, }; export function costOf(model, u) { const p = PRICES[model] ?? PRICES.large; const fresh = Math.max((u.input_tokens ?? 0) - (u.cache_read_input_tokens ?? 0), 0); return (fresh * p.in + (u.cache_read_input_tokens ?? 0) * p.cachedIn + (u.output_tokens ?? 0) * p.out) / 1e6; } export async function record({ feature, model = "large", usage = {}, ok = true, aborted = false, ms = 0 }) { const ctx = als.getStore() ?? {}; const line = { ts: Date.now(), feature, model, ok, aborted, ms, inputTokens: usage.input_tokens ?? 0, cachedTokens: usage.cache_read_input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0, usd: costOf(model, usage), version: process.env.GIT_SHA ?? "dev", ...ctx, }; await appendFile(process.env.LLM_LOG ?? "llm-spend.jsonl", JSON.stringify(line) + "\n"); } ``` Now every call inside a request is tagged with tenant and route, and nothing in your business logic knows about it. :::warn Floating-point money `costOf` returns a float, which is fine for a dashboard and wrong for an invoice. If these numbers ever reach a customer's bill, accumulate in integer micro-dollars — see [the failure-mode catalogue](/review/failure-modes/) on why `0.1 + 0.2` is a problem you do not want in a billing path. ::: ## Make the cache hit The largest saving available, and in JavaScript the usual killer is object property order. ```js // BAD — a fresh timestamp at the top invalidates everything after it const messages = [ { role: "system", content: `Today is ${new Date().toISOString()}\n${RULES}` }, { role: "user", content: docs.join("\n") + question }, ]; // GOOD — stable prefix, volatile tail const messages = [ { role: "system", content: RULES, cache_control: { type: "ephemeral" } }, { role: "user", content: [...docs].sort().join("\n"), cache_control: { type: "ephemeral" } }, { role: "user", content: `Today is ${new Date().toISOString()}\n${question}` }, ]; ``` `[...docs].sort()` matters. If your documents come from a `Set`, an `Object.values()`, or a query with no `ORDER BY`, the order can vary between calls — and a different order is a different prefix, which is a total cache miss on that block. It fails silently and shows up only as a bill. The same applies to tool definitions assembled from an object. Serialise them in a fixed order, because `JSON.stringify` follows insertion order and insertion order is easy to change by accident. ```js // a test that catches the day someone adds a uuid to the system prompt test("prompt prefix is stable across requests", () => { const a = buildMessages("q1", ["b", "a"]); const b = buildMessages("q2", ["a", "b"]); expect(a.slice(0, 2)).toEqual(b.slice(0, 2)); }); ``` ## Bound the fan-out `Promise.all` over an array has no concurrency limit. A thousand rows means a thousand simultaneous requests, an immediate 429 storm, retries on top, and a budget check that fires after you have already committed to everything. ```js async function mapLimit(items, limit, fn) { const out = new Array(items.length); let i = 0; await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); } })); return out; } const results = await mapLimit(rows, 8, classify); ``` Check the budget *inside* `fn`, not before the loop, so it can stop the run partway. ## Retries multiply ```js const RETRYABLE = new Set([408, 409, 429, 500, 502, 503, 504]); export async function withRetry(fn, { attempts = 3, base = 500 } = {}) { for (let n = 1; ; n++) { try { return await fn(); } catch (err) { const status = err.status ?? err.response?.status; if (n >= attempts || !RETRYABLE.has(status)) throw err; const jitter = Math.random() * base; await new Promise((r) => setTimeout(r, base * 2 ** (n - 1) + jitter)); } } } ``` Never retry a 400 — the request is malformed and will be malformed identically. Always jitter, or every worker in your fleet retries in lockstep and you turn a blip into an outage you also paid for. ## Cap the loop An agent loop resends the whole conversation each turn, so cost grows roughly quadratically with turn count. A ten-turn loop over 20k tokens of context is closer to 200k tokens than 20k. ```js export async function runAgent(messages, { maxTurns = 12, budgetUsd = 0.5 } = {}) { let spent = 0; for (let turn = 0; turn < maxTurns; turn++) { const { text, toolCalls, usage } = await llm.complete({ messages }); spent += costOf("large", usage); if (spent > budgetUsd) throw new Error(`budget exceeded after ${turn + 1} turns`); if (!toolCalls?.length) return text; messages = [...messages, ...(await runTools(toolCalls))]; } throw new Error(`hit turn limit (${maxTurns})`); } ``` A turn cap is the single cheapest protection against a runaway loop, and it is the one people add after the incident. ## The report ```js scripts/spend-report.js import { readFileSync } from "node:fs"; const rows = readFileSync("llm-spend.jsonl", "utf8").trim().split("\n").map(JSON.parse); const by = (key) => Object.entries( rows.reduce((acc, r) => { const k = r[key] ?? "unknown"; acc[k] ??= { calls: 0, usd: 0, ok: 0, aborted: 0, out: 0 }; acc[k].calls++; acc[k].usd += r.usd; acc[k].ok += r.ok ? 1 : 0; acc[k].aborted += r.aborted ? 1 : 0; acc[k].out += r.outputTokens; return acc; }, {}), ).sort((a, b) => b[1].usd - a[1].usd); console.table(Object.fromEntries(by("feature").map(([k, v]) => [k, { calls: v.calls, usd: v.usd.toFixed(4), usdPerSuccess: (v.usd / Math.max(v.ok, 1)).toFixed(5), // the number that matters abortRate: (v.aborted / v.calls).toFixed(3), // the Node-specific one }]))); const cacheRate = rows.reduce((s, r) => s + r.cachedTokens, 0) / Math.max(rows.reduce((s, r) => s + r.inputTokens, 0), 1); console.log("cache hit rate:", cacheRate.toFixed(3)); ``` `console.table` is underused and is exactly right here. :::verdict The four that usually do it in Node 1. **Cancel upstream on client disconnect.** The Node-specific one, and it is free money. 2. **Reorder prompts so the cache hits.** Largest single saving, changes no behaviour. 3. **Cap turns and bound fan-out concurrency.** Protects the worst case. 4. **Route classification to a small model, and set `max_tokens`.** Output is the expensive side. ::: ## Common questions ### How do I count tokens before sending, in Node? There are JavaScript ports of the common tokenisers, and they are fine for a budget check. Treat any local count as an estimate — provider tokenisers differ and none of them account for tool schemas or system scaffolding. Decide with the estimate, account with the `usage` the API returns. ### Does the abandoned-stream problem apply behind a proxy? Yes, and it can be worse: a proxy may hold the connection open after the browser is gone, so your handler never sees a close. Add a server-side timeout as well as the disconnect handler, and check that your proxy propagates client aborts. ### Should I record spend to a file or a database? A file to start with — JSONL, append-only, no schema migration, and the report above reads it in ten lines. Move it to your metrics backend when the volume justifies it. What matters from day one is *what* you tag, not where it lands, because tags are nearly impossible to backfill. ### Is Node a bad choice for a high-volume LLM gateway? No, but the failure modes are different: streaming lifecycle bugs and unbounded concurrency rather than CPU. If your gateway is doing heavy fan-out with strict budget enforcement, [Go](https://learn-go.org/ai/tokenomics/) makes the cancellation and concurrency parts easier to get right. ## Conditions Source: https://learn-javascript.org/conditions/ ### The `if` statement The `if` statement allows us to check if an expression is equal to `true` or `false`, and execute different code according to the result. For example, if we want ask the user whether his name is “John”, we can use the `confirm` function. ```javascript if (confirm("Are you John Smith?")) { console.log("Hello John, how are you?"); } else { console.log("Then what is your name?"); } ``` It is also possible to omit the `else` keyword if we only want to execute a block of code only if a certain expression is true. To evaluate whether two variables are equal, the `==` operator is used. There is also another equality operator in JavaScript, `===`, which does a strict comparison. This means that it will be true only if the two things you are comparing are the same type as well as same content. ```javascript console.log("1" == 1); // true console.log("1" === 1); // false ``` For example: ```javascript let myNumber = 42; if (myNumber == 42) { console.log("The number is correct."); } ``` Inequality operators can also be used to evaluate expressions. For example: ```javascript let foo = 1; let bar = 2; if (foo < bar) { console.log("foo is smaller than bar."); } ``` Two or more expressions can be evaluated together using logical operators to check if two expressions evaluate to `true` together, or at least one of them. To check if two expressions both evaluate to `true`, use the AND operator `&&`. To check if at least one of the expressions evaluate to `true`, use the OR operator `||`. ```javascript let foo = 1; let bar = 2; let moo = 3; if (foo < bar && moo > bar) { console.log("foo is smaller than bar AND moo is larger than bar."); } if (foo < bar || moo > bar) { console.log("foo is smaller than bar OR moo is larger than bar."); } ``` The NOT operator `!` can also be used likewise: ```javascript let notTrue = false; if (!notTrue) { console.log("not not true is true!"); } ``` ### The `switch` statement The `switch` statement is similar to the `switch` statement from the C programming language, but also supports strings. The `switch` statement is used to select between more than two different options, and to run the same code for more than one option. For example: ```javascript let rank = "Commander"; switch(rank) { case "Private": case "Sergeant": console.log("You are not authorized."); break; case "Commander": console.log("Hello commander! what can I do for you today?"); break; case "Captain": console.log("Hello captain! I will do anything you wish."); break; default: console.log("I don't know what your rank is."); break; } ``` In this example, “Private” an “Sergeant” both trigger the first sentence, “Commander” triggers the second sentence and “Captain” triggers the third. If an unknown rank was evaulated, the `default` keyword defines the action for this case (optional). We must use the `break` statement between every code block to avoid the `switch` from executing the next code block. Using the `switch` statement in general is not recommended, because forgetting the `break` keyword causes very confusing results. ## Loops Source: https://learn-javascript.org/loops/ ### The for statement JavaScript has two methods for running the same code several times. It is mainly used for iterating over arrays or objects. Let’s see an example: ```javascript let i; for (i = 0; i < 3; i = i + 1) { console.log(i); } ``` This will print out the following: ```javascript 0 1 2 ``` The `for` statement in JavaScript has the same syntax as in Java and C. It has three parts: - **Initialization** - Initializes the iterator variable `i`. In this example, we initialize `i` to 0. - **Condition** - As long as the condition is met, the loop continues to execute. In this example, we check that `i` is less than 3. - **Increment** - A directive which increments the iterator. In our case, we increment it by 1 on every loop. We can also write a shorter notation for the statement by inserting the variable definition inside the `for` loop and incrementing using the `++` operator. ```javascript for (let i = 0; i < 3; i++) { console.log(i); } ``` To iterate over an array and print out all of its members, we usually use the `for` statement. Here’s an example: ```javascript let myArray = ["A", "B", "C"]; for (let i = 0; i < myArray.length; i++) { console.log("The member of myArray in index " + i + " is " + myArray[i]); } ``` This prints out the contents of the array: ```javascript The member of myArray in index 0 is A The member of myArray in index 1 is B The member of myArray in index 2 is C ``` Notice that we used the `length` property of an array, which returns the number of members in the array, so we know when to stop iterating. ### The while statement The `while` statement is a more simple version of the `for` statement which checks if an expression evaluates to `true` and runs as long as it says `true`. For example: ```javascript let i = 99; while (i > 0) { console.log(i + " bottles of beer on the wall"); i -= 1; } ``` ### break and continue statements The `break` statement allows to stop the execution of a loop. For example, we can create a loop that loops forever using `while(true)` and use the `break` statement to break inside the loop instead by checking that a certain condition was met. ```javascript let i = 99; while (true) { console.log(i + " bottles of beer on the wall"); i -= 1; if (i == 0) { break; } } ``` The `continue` statement skips the rest of the loop and jumps back to the beginning of the loop. For example, if we would want to print only odd numbers using a `for` statement, we can do the following: ```javascript for (let i = 0; i < 100; i++) { // check that the number is even if (i % 2 == 0) { continue; } // if we got here, then i is odd. console.log(i + " is an odd number."); } ``` ## Objects Source: https://learn-javascript.org/objects/ JavaScript is a functional language, and for object oriented programming it uses both objects and functions, but objects are usually used as a data structure, similar to a dictionary in Python or a map in Java. In this tutorial, we will learn how to use objects as a data structure. The advanced tutorials explain more about object oriented JavaScript. To initialize an object, use curly braces: ```javascript let emptyObject = {}; let personObject = { firstName : "John", lastName : "Smith" } ``` ### Member addressing Members of objects can be addressed using the brackets operator `[]`, very much like arrays, but just like many other object oriented languages, the period `.` operator can also be used. They are very similar, except for the fact that brackets return a member by using a string, in contrast to the period operator, which requires the member to be a simple word (the word should not contain spaces, start with a number or use illegal characters). For example, we can continue to fill the person object with more details: ```javascript let personObject = { firstName : "John", lastName : "Smith" } personObject.age = 23; personObject["salary"] = 14000; ``` ### Iteration Iterating over members of a dictionary is not a trivial task, since iterating over objects can also yield members who don’t actually belong to an object. Therefore, we must use the `hasOwnProperty` method to check that the member in fact belongs to the object. ```javascript for (let member in personObject) { if (personObject.hasOwnProperty(member)) { console.log("the member " + member + " of personObject is " + personObject[member]) } } ``` This will eventually print out ```javascript the member firstName of personObject is John the member lastName of personObject is Smith the member age of personObject is 23 the member salary of personObject is 14000 ``` Note that methods of objects in JavaScript have a fixed order, like arrays. ## Functions Source: https://learn-javascript.org/functions/ Functions are code blocks that can have arguments, and function have their own scope. In JavaScript, functions are a very important feature of the program, and especially the fact that they can access local variables of a parent function (this is called a closure). There are two ways to define functions in JavaScript - named functions and anonymous functions. To define a named function, we use the `function` statement as follows: ```javascript function greet(name) { return "Hello " + name + "!"; } console.log(greet("Eric")); // prints out Hello Eric! ``` In this function, the `name` argument to the `greet` function is used inside the function to construct a new string and return it using the `return` statement. To define an anonymous function, we can alternatively use the following syntax: ```javascript let greet = function(name) { return "Hello " + name + "!"; } console.log(greet("Eric")); // prints out Hello Eric! ``` ## Pop-up Boxes Source: https://learn-javascript.org/pop-up-boxes/ There are three types of pop-up boxes in javascript: confirm, alert, and prompt. To use any of them, type ```javascript confirm("Hi!"); prompt("Bye!"); alert("Hello"); ``` Confirm boxes will return “true” if ok is selected, and return “false” if cancel is selected. Alert boxes will not return anything. Prompt boxes will return whatever is in the text box. Note: prompt boxes also have an optional second parameter, which is the text that will already be in the text box. ## Callbacks Source: https://learn-javascript.org/callbacks/ Callbacks in JavaScript are functions that are passed as arguments to other functions. This is a very important feature of asynchronous programming, and it enables the function that receives the callback to call our code when it finishes a long task, while allowing us to continue the execution of the code. For example: ```javascript let callback = function() { console.log("Done!"); } setTimeout(callback, 5000); ``` This code waits 5 seconds and prints out “Done!” when the 5 seconds are up. Note that this code will not work in the interpreter because it is not designed for handling callbacks. It is also possible to define callbacks as anonymous functions, like so: ```javascript setTimeout(function() { console.log("Done!"); }, 5000); ``` Like regular functions, callbacks can receive arguments and be executed more than once. ## Arrow Functions Source: https://learn-javascript.org/arrow-functions/ Arrow functions are a feature of ES6, their behavior are generally the same of a function. These are anonymous functions with a special syntax, they haven’t their own this, arguments or super. They can’t be used as constructors too. Arrow functions are often used as callbacks of native JS functions like map, filter or sort. The reason of their name is due to the use of `=>` in the syntax. To define an arrow function, we use the `() => {}` structure as follows: ```javascript const greet = (name) => { return "Hello " + name + "!"; } console.log(greet("Eric")); // prints out Hello Eric! ``` In this function, the `name` argument to the `greet` function is used inside the function to construct a new string and return it using the `return` statement. In case that the function only receives one argument, we can omit the parenthesis: ```javascript const greet = name => { return "Hello " + name + "!"; } console.log(greet("Eric")); // prints out Hello Eric! ``` And, in case that we want to do a explicit return of the function and we have only one line of code, we can avoid the `return` statement and omit brackets too: ```javascript const greet = name => "Hello " + name + "!"; console.log(greet("Eric")); // prints out Hello Eric! ``` Using an arrow as a callback compared to a normal function: ```javascript let numbers = [3, 5, 8, 9, 2]; // Old way function multiplyByTwo(number){ return number * 2; } let multipliedNumbers = numbers.map(multiplyByTwo); console.log(multipliedNumbers); // prints out: 6, 10, 16, 18, 4 // Using ES6 arrow functions const multiplyByTwo = number => number * 2; let multipliedNumbers = numbers.map(multiplyByTwo); console.log(multipliedNumbers); // prints out: 6, 10, 16, 18, 4 ``` ## Object Oriented JavaScript Source: https://learn-javascript.org/object-oriented-javascript/ JavaScript uses functions as classes to create objects using the `new` keyword. Here is an example: ```javascript function Person(firstName, lastName) { // construct the object using the arguments this.firstName = firstName; this.lastName = lastName; // a method which returns the full name this.fullName = function() { return this.firstName + " " + this.lastName; } } let myPerson = new Person("John", "Smith"); console.log(myPerson.fullName()); // outputs "John Smith" ``` Creating an object using the `new` keyword is the same as writing the following code: ```javascript let myPerson = { firstName : "John", lastName : "Smith", fullName : function() { return this.firstName + " " + this.lastName; } } ``` The difference between the two methods of creating objects is that the first method uses a class to define the object and then the `new` keyword to instantiate it, and the second method immediately creates an instance of the object. ## Function Context Source: https://learn-javascript.org/function-context/ Functions in JavaScript run in a specific context, and using the `this` variable we have access to it. All standard functions in the browser run under the Window context. Functions defined under an object or a class (another function) will use the context of the object it was created in. However, we can also change the context of a function at runtime, either before or while executing the function. ### Binding a method to an object To bind a function to an object and make it an object method, we can use the `bind` function. Here is a simple example: ```javascript let person = { name : "John" }; function printName() { console.log(this.name); } ``` Obviously, we are not able to call `printName()` without associating the function with the object `person`. To do this we must create a bound method of the function printName to person, using the following code: ```javascript let boundPrintName = printName.bind(person); boundPrintName(); // prints out "John" ``` ### Calling a function with a different context We can use the `call` and `apply` functions to call a function as if it was bound to an object. The difference between the `call` and `apply` functions is only by how they receive their arguments - the `call` function receives the `this` argument first, and afterwards the arguments of the function, whereas the `apply` function receives the `this` argument first, and an array of arguments to pass on to the function as a second argument to the function. For example, let’s call `printName` with `person` as the context using the `call` method: ```javascript printName.call(person); // prints out "John" ``` ### call/apply vs bind The difference between `call`/`apply` and `bind` is that `bind` returns a new function identical to the old function, except that the value of `this` in the new function is now the object it was bound to. `call`/`apply` calls the function with `this` being the bound object, but it does not return a return a new function or change the original, it calls it with a different value for `this`. For example: ```javascript let boundPrintName = printName.call(person); //boundPrintName gets printName's return value (null) boundPrintName(); //doesn't work because it's not a function, it's null printName.bind(person); //returns a new function, but nothing is using it so it's useless printName(); //throws error because this.name is not defined ``` Think of `call` as executing the return value of `bind`. For example: ```javascript printName.call(person); //is the same as printName.bind(person)(); //executes the function returned by bind ``` Or think of `bind` returning a shortcut to `call`. For example: ```javascript let boundPrintName = printName.bind(person); //is the same as let boundPrintName = function() { printName.call(person); } ``` ## Inheritance Source: https://learn-javascript.org/inheritance/ JavaScript uses prototype based inheritance. Every object has a `prototype`, and when a method of the object is called then JavaScript tries to find the right function to execute from the prototype object. ### The prototype attribute Without using the prototype object, we can define the object Person like this: ```javascript function Person(name, age) { this.name = name; this.age = age; function describe() { return this.name + ", " + this.age + " years old."; } } ``` When creating instances of the `Person` object, we create a new copy of all members and methods of the functions. This means that every instance of an object will have its own `name` and `age` properties, as well as its own `describe` function. However, if we use the `Person.prototype` object and assign a function to it, it will also work. ```javascript function Person(name, age) { this.name = name; this.age = age; } Person.prototype.describe = function() { return this.name + ", " + this.age + " years old."; } ``` When creating instances of the `Person` object, they will not contain a copy of the `describe` function. Instead, when calling an object method, JavaScript will attempt to resolve the `describe` function first from the object itself, and then using its `prototype` attribute. ### Inheritance Let’s say we want to create a `Person` object, and a `Student` object derived from `Person`: ```javascript let Person = function() {}; Person.prototype.initialize = function(name, age) { this.name = name; this.age = age; } Person.prototype.describe = function() { return this.name + ", " + this.age + " years old."; } let Student = function() {}; Student.prototype = new Person(); Student.prototype.learn = function(subject) { console.log(this.name + " just learned " + subject); } let me = new Student(); me.initialize("John", 25); me.learn("Inheritance"); ``` As we can see in this example, the `initialize` method belongs to `Person` and the `learn` method belongs to `Student`, both of which are now part of the `me` object. Keep in mind that there are many ways of doing inheritance in JavaScript, and this is just one of them. ## Destructuring Source: https://learn-javascript.org/destructuring/ Destructuring is a feature of ES6, introduced for making easier and cleaner some repetitive operations and assignments made in JS. With destructuring we can data from a deeper lever inside an array / object with a more concise syntax, even giving to this ‘extracted’ data other name in the same operation. In JavaScript we can achieve this in a very simply way: ```javascript // Consider this object const person = { head: { eyes: 'x', mouth: { teeth: 'x', tongue: 'x' } }, body: { shoulders: 'x', chest: 'x', arms: 'x', hands: 'x', legs: 'x' } }; // If we want to get head, the old way: let head = person.head; // ES6 Destructuring let { head } = person; // We can give other name as if a variable was declared, in the same line let { head : myHead } = person; // So we can do... console.log(myHead); // prints '{ eyes, mouth: { ... } }' ``` With arrays: ```javascript let numbers = ['2', '3', '7']; // Old way let two = numbers[0]; let three = numbers[1]; // ES6 Destructuring let [two, three] = numbers; // We can give them other names too let [two: positionZero, three: positionOne] = numbers; console.log(positionZero) // prints '2' console.log(positionOne) // prints '3' ``` We can do this with function parameters too: ```javascript // Old way function getHeadAndBody(person) { let headAndBody = { head: person.head, body: person.body } return headAndBody; } // ES6 Destructuring function getHeadAndBody({ head, body }) { return { head, body } } // With arrow functions let getHeadAndBody = ({ head, body }) => { head, body }; ``` Warning: Be careful with destructuring, if you aren’t sure if the function is going to receive an object with those parameters, it’s better to use the old way in order to not incurring in `head / body is undefined` errors. You can avoid that type of errors while using ES6 Destructuring giving default parameters to the function, so you can be sure that properties will exist, not being obliged to rely on the parameters received. ```javascript // I'm not sure if head and body will be present in some cases... // Now we are sure that head or body will be equal to '' if the real parameter doesn't have that properties inside function getHeadAndBody({ head = '', body = '' }) { return { head, body } } ``` You can destructure as deep as you like, always considering if that property exists. ```javascript // Deep destructuring let computer = { processor: { transistor: { silicon: { thickness: '9nm' } } } } let { processor: { transistor: { silicon: { thickness } } } } = computer; // Making it cleaner let { thickness: inteli9Thickness } = computer.processor.transistor.silicon; console.log(inteli9Thickness) // prints '9nm' ``` ## Promises and async/await Source: https://learn-javascript.org/promises-and-async-await/ JavaScript is single-threaded. Anything slow — a network request, a file read, a timer — has to happen without blocking, and a `Promise` is the object representing a value that is not ready yet. ```javascript const promise = fetch("/api/users/1"); // a Promise, immediately ``` A promise is **pending**, then either **fulfilled** with a value or **rejected** with a reason. Once settled it never changes. ## async/await ```javascript async function getUser(id) { const res = await fetch(`/api/users/${id}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.json(); } const user = await getUser("1"); ``` `await` pauses the function until the promise settles, then gives you the value. Two rules: - An `async` function **always** returns a promise, even if you return a plain value. - `await` only works inside an `async` function, or at the top level of a module. ```javascript async function answer() { return 42; } answer(); // Promise { 42 }, not 42 await answer(); // 42 ``` ## Error handling ```javascript try { const user = await getUser("1"); console.log(user.email); } catch (err) { console.error("failed:", err.message); } finally { setLoading(false); // runs either way } ``` A rejected promise inside `await` throws, so `try/catch` works exactly as it does for synchronous code. That is the main reason `async/await` replaced `.then()` chains. :::warn `fetch` does not reject on a 404 ```javascript const res = await fetch("/api/missing"); // resolves fine ``` `fetch` only rejects on a *network* failure. A 404 or 500 is a successful HTTP exchange as far as it is concerned. You must check `res.ok` yourself — omitting it is one of the most common bugs in generated JavaScript. ::: ## Concurrency The difference that most affects real performance: ```javascript // SEQUENTIAL — three round trips, one after another const user = await getUser(id); const orders = await getOrders(id); const prefs = await getPrefs(id); // CONCURRENT — all three at once const [user, orders, prefs] = await Promise.all([ getUser(id), getOrders(id), getPrefs(id), ]); ``` Both are correct. The second takes a third of the time. **Look for consecutive `await` lines with no data dependency** — that is the signal. The same problem in a loop: ```javascript // 100 sequential requests for (const id of ids) { results.push(await fetchUser(id)); } // all at once const results = await Promise.all(ids.map(fetchUser)); ``` ### But do not go unbounded ```javascript await Promise.all(ids.map(fetchUser)); // with 50,000 ids: a 429 storm ``` When the array size comes from data rather than from your source code, bound the concurrency: ```javascript async function mapLimit(items, limit, fn) { const out = new Array(items.length); let i = 0; await Promise.all(Array.from({ length: Math.min(limit, items.length) }, async () => { while (i < items.length) { const idx = i++; out[idx] = await fn(items[idx], idx); } })); return out; } const results = await mapLimit(ids, 8, fetchUser); ``` ## The four combinators ```javascript Promise.all([...]) // all succeed, or reject on the first failure Promise.allSettled([...]) // never rejects; reports each outcome Promise.race([...]) // first to settle, success or failure Promise.any([...]) // first to SUCCEED; rejects only if all fail ``` `allSettled` is the one people under-use. For "fetch these ten things and show me what you got", `all` is wrong — one failure discards nine successes. ```javascript const results = await Promise.allSettled(ids.map(fetchUser)); for (const r of results) { if (r.status === "fulfilled") console.log(r.value.email); else console.error(r.reason); } ``` ## The floating promise The most common bug in generated JavaScript: ```javascript async function save(user) { db.write(user); // not awaited return { ok: true }; } ``` The function returns before the write happens, any error becomes an unhandled rejection, and since Node 15 an unhandled rejection **terminates the process**. Two variants worth recognising: ```javascript // forEach ignores the returned promise entirely items.forEach(async (item) => { await process(item); }); console.log("done"); // prints immediately; nothing is done // a promise is always truthy if (isReady()) { } // isReady is async — this is always true ``` Turn on the rules that catch these: ```javascript "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-misused-promises": "error", ``` They need type information, which you can get on plain JavaScript with a `jsconfig.json` containing `checkJs: true`. That one file is worth it for this rule alone. ## Creating a promise Usually you do not — you get promises from APIs. The exception is wrapping an old callback API: ```javascript function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } await delay(1000); ``` In Node, `util.promisify` or the `node:fs/promises` style APIs already exist for most of the standard library, so reach for those first. ## Cancellation ```javascript const controller = new AbortController(); setTimeout(() => controller.abort(), 5000); try { const res = await fetch(url, { signal: controller.signal }); } catch (err) { if (err.name === "AbortError") console.log("cancelled"); else throw err; } ``` `AbortSignal.timeout(5000)` does the same in one line. Threading a signal through anything that talks to a network is what makes it cancellable — and on a server, what stops you doing work for a client that has already left. ## Exercise ```javascript // Given getUser(id), getOrders(id) and getPrefs(id) (all async): // 1. Write loadDashboard(id) fetching all three CONCURRENTLY, // returning { user, orders, prefs }. // 2. Handle failure so one broken call does not lose the other two. // 3. Add a 3-second timeout to the whole thing. console.log("start"); ``` ## Common questions ### `.then()` or `async/await`? `async/await` for almost everything — it reads linearly and errors work with ordinary `try/catch`. `.then()` still suits short transformations and places where you cannot be in an async function. Do not mix both styles in one function. ### Why does my `try/catch` not catch the error? Most likely the promise was not awaited, so the error happened after your `try` block finished. Errors thrown inside a callback that runs later — `setTimeout`, an event handler — also cannot be caught by the surrounding `try`, because that stack is gone by then. ### Is `await` in a loop always wrong? No. It is right when each iteration depends on the previous one, or when you are deliberately rate-limiting. It is wrong when the iterations are independent, which is the common case — and that is why `no-await-in-loop` is best set as a warning rather than an error. ## Modules Source: https://learn-javascript.org/modules/ A module is a file whose top-level declarations are private unless exported. ```javascript src/pricing.js export const TAX_RATE = 0.2; export function total(items) { return items.reduce((sum, i) => sum + i.priceCents, 0); } function internalHelper() {} // not exported — invisible outside this file ``` ```javascript src/cart.js import { total, TAX_RATE } from "./pricing.js"; console.log(total(items) * (1 + TAX_RATE)); ``` ## Import forms ```javascript import { total } from "./pricing.js"; // named import { total as cartTotal } from "./pricing.js"; // renamed import * as pricing from "./pricing.js"; // everything as a namespace import formatter from "./format.js"; // the default export import formatter, { currency } from "./format.js"; // both import "./side-effects.js"; // run it, import nothing ``` ## Named over default ```javascript export default function parse() {} // works, and prefer not to export function parse() {} // better ``` Named exports are worth defaulting to. The name is fixed at the source, so every import uses the same word, rename refactors work across the codebase, and editor auto-import is reliable. A default export can be imported under any name, which is how one function ends up called three things. ## Dynamic import `import()` is a function that returns a promise, so it can run conditionally and at runtime: ```javascript if (user.wantsCharts) { const { renderChart } = await import("./charts.js"); renderChart(data); } ``` The chart library is only downloaded and parsed if it is needed. This is the mechanism behind code splitting, and on the server it is how you avoid paying startup cost for a path most requests never take. ## ESM and CommonJS The long-running source of confusion. Node originally had its own module system, CommonJS, and later gained the standard one, ESM. ```javascript // CommonJS — the old system const { total } = require("./pricing"); module.exports = { total }; // ESM — the standard import { total } from "./pricing.js"; export { total }; ``` Which one a `.js` file uses is decided by the nearest `package.json`: ```json package.json { "type": "module" } // .js files are ESM { "type": "commonjs" } // .js files are CommonJS (the default if absent) ``` You can also be explicit per file: `.mjs` is always ESM, `.cjs` is always CommonJS. **Use ESM for anything new.** It is the standard, it works in browsers, it supports top-level `await`, and the ecosystem has largely moved. :::warn The extension is required in ESM ```javascript import { total } from "./pricing"; // fails in Node ESM import { total } from "./pricing.js"; // correct ``` CommonJS guessed at extensions and index files; ESM does not. Bundlers often paper over this, which is why code works in a bundled app and fails when run directly by Node. ::: ### The differences that bite | | CommonJS | ESM | |---|---|---| | Loading | synchronous, at runtime | asynchronous, statically analysed | | `__dirname` | available | use `import.meta.dirname` | | Top-level `await` | no | yes | | Importing the other | can `require` CJS only | can import CJS, with caveats | | Conditional import | `require()` anywhere | `await import()` | ```javascript // ESM equivalents of the CommonJS globals import.meta.dirname // __dirname import.meta.filename // __filename import.meta.url // the module's URL ``` A CommonJS file **cannot** `require()` an ESM module. That is the source of "this package is ESM-only" — the fix is to move your project to ESM, or use `await import()` from an async context. ## Circular imports ```javascript // a.js import { b } from "./b.js"; export const a = () => b(); // b.js import { a } from "./a.js"; export const b = () => a(); ``` ESM handles cycles better than CommonJS, but they still produce partially-initialised modules and confusing `undefined` errors. A cycle almost always means two modules share a concept that wants extracting into a third. ## Import maps and bare specifiers ```javascript import { total } from "./pricing.js"; // relative — a file path import express from "express"; // bare — resolved from node_modules ``` In the browser, bare specifiers need an import map: ```html ``` ## Exercise ```javascript // Split this into modules: // math.js — exports add and multiply (named) // format.js — exports a default currency formatter // main.js — imports both, and dynamically imports a "stats.js" // module only when an array has more than 10 items console.log("start"); ``` ## Common questions ### Why does my import fail without a file extension? ESM requires the full specifier, including `.js`. CommonJS guessed at extensions and index files and ESM deliberately does not, because the resolution has to be statically analysable. Bundlers hide this, which is why it appears only when Node runs the file directly. ### Should I use default or named exports? Named. Fixed names mean consistent imports, working rename refactors and reliable auto-import. Reserve the default export for a module with one obvious primary thing, and even then a named export costs nothing. ### How do I use an ESM-only package from CommonJS? You cannot `require()` it. Either convert your project to ESM by adding `"type": "module"`, or use `await import()` from inside an async function. The first is the real fix; the second is a workaround for a file you cannot convert yet. ## Error Handling Source: https://learn-javascript.org/error-handling/ ```javascript try { const data = JSON.parse(input); process(data); } catch (err) { console.error("could not parse:", err.message); } finally { cleanup(); // runs whether or not it threw } ``` ## The Error object ```javascript throw new Error("something broke"); const err = new Error("boom"); err.name; // "Error" err.message; // "boom" err.stack; // where it was created ``` Built-in subclasses carry meaning: ```javascript TypeError // wrong type — null.foo, undefined is not a function RangeError // out of range — new Array(-1) SyntaxError // bad syntax — JSON.parse("{") ReferenceError // undeclared variable ``` **Always throw an `Error`, never a string.** A thrown string has no stack trace, and every `catch` block that does `err.message` gets `undefined`. ```javascript throw "not found"; // no stack, no name, breaks every handler throw new Error("not found"); // correct ``` ## Custom error classes ```javascript class NotFoundError extends Error { constructor(resource, id) { super(`${resource} ${id} not found`); this.name = "NotFoundError"; this.resource = resource; this.id = id; } } class ValidationError extends Error { constructor(field, reason) { super(`${field}: ${reason}`); this.name = "ValidationError"; this.field = field; } } ``` Now callers can distinguish failures and respond appropriately: ```javascript try { await createOrder(payload); } catch (err) { if (err instanceof ValidationError) return respond(400, { field: err.field }); if (err instanceof NotFoundError) return respond(404); throw err; // not ours — let it propagate } ``` That final `throw err` matters. Catching an error you cannot handle and swallowing it is how bugs become invisible. ## Preserving the cause ```javascript try { await db.query(sql); } catch (err) { throw new Error(`loading user ${id} failed`, { cause: err }); } ``` The `cause` option keeps the original error attached, so you get context *and* the underlying failure. Without it you either lose the detail or lose the context. ```javascript catch (err) { console.error(err.message); // "loading user 1 failed" console.error(err.cause.message); // "connection refused" } ``` ## Async errors ```javascript try { const user = await getUser(id); } catch (err) { // works — a rejected promise throws at the await } ``` But only if you `await`. Without it, the error escapes: ```javascript try { getUser(id); // not awaited } catch (err) { // never runs. the rejection happens after this block. } ``` :::danger An unhandled rejection terminates the process Since Node 15 the default for an unhandled promise rejection is to crash. A single un-awaited call in a background job can take down your whole server. ```javascript process.on("unhandledRejection", (reason) => { logger.fatal({ reason }, "unhandled rejection"); process.exit(1); }); ``` Log it and exit deliberately rather than letting it crash silently — but treat every occurrence as a bug to fix, not a condition to handle. The real fix is `no-floating-promises` in your linter. ::: ## Errors in callbacks ```javascript try { setTimeout(() => { throw new Error("boom"); }, 0); } catch (err) { // never runs } ``` The callback runs later, on a fresh stack. The `try` block has long since finished. Anything asynchronous needs its error handling *inside* the callback, or needs to be promise-based so `await` can catch it. ## What not to do **Swallowing:** ```javascript try { risky(); } catch (err) {} // the incident starts here try { risky(); } catch (err) { console.log(err); } // barely better ``` If you cannot handle it, do not catch it. If you catch it deliberately — a cache read that is allowed to fail — say so in a comment. **Catching too broadly:** ```javascript try { const data = JSON.parse(input); await save(data); // a save failure now looks like a parse failure } catch (err) { return { error: "invalid JSON" }; } ``` Keep the `try` around the operation you are actually handling. **Returning error codes instead of throwing:** ```javascript const result = doThing(); if (result === -1) { } // easy to ignore; no stack; no message ``` Throw, or return a discriminated result object (`{ ok: true, value }` / `{ ok: false, error }`) — but pick one convention per codebase. ## Result objects For expected failures, a returned result is often better than an exception, because the type system and the reader can both see it: ```javascript async function findUser(id) { const row = await db.get(id); return row ? { ok: true, value: row } : { ok: false, error: "not_found" }; } const result = await findUser(id); if (!result.ok) return respond(404); use(result.value); ``` Reserve exceptions for the genuinely exceptional — a database being down, a bug — and use results for outcomes you expect, like "not found" or "invalid input". ## Exercise ```javascript // 1. Write a ValidationError class carrying a `field`. // 2. Write validateOrder(order) that throws it for a missing customerId // or an empty items array. // 3. Write handleRequest(order) that catches ValidationError and returns // { status: 400, field }, re-throws anything else, and wraps a database // failure with `cause` preserved. console.log("start"); ``` ## Common questions ### Should I throw or return an error? Throw for the unexpected — a bug, a dependency that is down, a broken invariant. Return a result for expected outcomes like "not found" or "invalid input", where the caller must handle it anyway and an exception is control flow in disguise. ### Why is my `catch` not catching? The three usual causes: the promise was not awaited, the error was thrown inside a later callback, or something between you and the throw already caught and swallowed it. The first is by far the most common. ### Is it ever fine to catch and ignore? Occasionally, and it should look deliberate: a cache read that may fail, a best-effort cleanup. Write the comment saying why, so the next reader knows it is a decision rather than an oversight. ## Closures and Scope Source: https://learn-javascript.org/closures-and-scope/ A closure is a function that remembers the variables where it was defined, even after that outer function has returned. ```javascript function counter() { let count = 0; // lives on, because the inner function uses it return function () { count += 1; return count; }; } const next = counter(); next(); // 1 next(); // 2 const other = counter(); other(); // 1 — its own independent count ``` `count` is not garbage collected when `counter()` returns, because the returned function still references it. Each call to `counter()` creates a fresh binding. ## Scope ```javascript let outer = "visible everywhere below"; function example() { let fnScoped = "visible in this function"; if (true) { let blockScoped = "only in this block"; var functionScoped = "hoisted to the whole function"; } console.log(blockScoped); // ReferenceError console.log(functionScoped); // works — this is why let is a problem } ``` `let` and `const` are **block-scoped**; `var` is **function-scoped** and hoisted. That difference is the single best reason never to write `var`. ## The classic loop bug The most cited closure gotcha, and a good demonstration of why: ```javascript const fns = []; for (var i = 0; i < 3; i++) { fns.push(() => console.log(i)); } fns.forEach((f) => f()); // 3, 3, 3 ``` There is only **one** `i` — `var` gives the whole function one binding — and by the time the callbacks run, the loop has finished and `i` is 3. ```javascript for (let i = 0; i < 3; i++) { fns.push(() => console.log(i)); } fns.forEach((f) => f()); // 0, 1, 2 ``` `let` creates a **new binding per iteration**, so each closure captures its own. Using `let` in loops fixes this for free. ## Private state Before `#private` class fields, closures were how JavaScript did encapsulation — and they are still a clean way to do it: ```javascript function createAccount(openingCents) { let balanceCents = openingCents; // genuinely inaccessible outside return { deposit(cents) { if (cents <= 0) throw new RangeError("must be positive"); balanceCents += cents; }, get balance() { return balanceCents / 100; }, }; } const acc = createAccount(1000); acc.deposit(500); acc.balance; // 15 acc.balanceCents; // undefined — there is no way to reach it ``` No `this`, no `new`, no binding problems. For many objects this is simpler than a class. ## Partial application ```javascript function withPrefix(prefix) { return (message) => `[${prefix}] ${message}`; } const warn = withPrefix("WARN"); warn("disk almost full"); // "[WARN] disk almost full" ``` The returned function has `prefix` baked in. This is the pattern behind most middleware and configuration helpers. ## Closures and memory The part that causes real bugs. **A closure keeps everything in its scope alive**, not just the variables it uses: ```javascript function makeHandler(hugeBuffer) { const size = hugeBuffer.length; return () => console.log(size); // still retains hugeBuffer in many engines } ``` The safe pattern is to extract what you need and let the rest go: ```javascript function makeHandler(hugeBuffer) { const size = hugeBuffer.length; return () => console.log(size); } // call it as: makeHandler(buf); buf = null; ``` The more common leak is a listener that is never removed: ```javascript function subscribe(bigObject) { emitter.on("tick", () => use(bigObject)); // never removed } ``` Every call adds a listener, each holding its captured scope, and the emitter's array grows forever. Node's `MaxListenersExceededWarning` is the tell — treat it as a leak report, not noise. ```javascript function subscribe(bigObject) { const handler = () => use(bigObject); emitter.on("tick", handler); return () => emitter.off("tick", handler); // give callers a way to unsubscribe } ``` Returning an unsubscribe function is the pattern worth adopting everywhere — it is what React's `useEffect` cleanup is. ## Where you meet closures without noticing ```javascript // every callback items.map((item) => item.price * taxRate); // captures taxRate // event handlers button.addEventListener("click", () => save(form)); // React hooks — a stale closure is the most common hook bug useEffect(() => { const id = setInterval(() => setCount(count + 1), 1000); return () => clearInterval(id); }, []); // count is captured once and never updates — use the updater form ``` That last one is worth recognising: with an empty dependency array the effect captures `count` from the first render forever. `setCount((c) => c + 1)` avoids the capture entirely. ## Exercise ```javascript // 1. Write once(fn) returning a function that runs fn only the first time // and returns the cached result after that. // 2. Write createRateLimiter(maxCalls, windowMs) using closure state. // 3. Explain in a comment why this logs 3,3,3 and fix it: const fns = []; for (var i = 0; i < 3; i++) fns.push(() => console.log(i)); fns.forEach((f) => f()); ``` ## Common questions ### Are closures slow? No — they are fundamental to how JavaScript works and engines optimise them heavily. The cost is memory, not speed: a closure keeps its scope alive, so a long-lived one holding a large object keeps that object alive too. ### When should I use a closure instead of a class? When you want genuine privacy with no `this`, or when an object has a small, fixed set of behaviours. Classes suit types with many methods, inheritance, or where a framework expects them. For a counter, a rate limiter or a configured helper, a closure is usually simpler. ### Why does my React state seem stale inside a callback? Because the callback captured the value from the render it was created in. Either add the value to the dependency array so a fresh closure is made, or use the functional updater form, which reads the current value rather than a captured one. ## TensorFlow.js Source: https://learn-javascript.org/tensorflow-js/ ## TensorFlow.js Tutorial TensorFlow.js is an open-source hardware-accelerated JavaScript library for training and deploying machine learning models. It allows you to develop ML models in JavaScript and use them in the browser or in Node.js. ### What You'll Learn - Basic tensor operations and data manipulation - Creating and training simple neural networks - Using pre-trained models for image classification - Building a complete machine learning workflow ### Installation You can use TensorFlow.js in your project by including it via a CDN: ```javascript ``` ### 1. Understanding Tensors - The Building Blocks Tensors are the core data structure in TensorFlow.js. Let's start by understanding how to create and manipulate them. ```javascript
``` ### 2. Tensor Operations Learn how to perform mathematical operations on tensors. ```javascript
``` ### 3. Hello World Example: Linear Regression Let's create a simple linear regression model to predict a value based on a linear relationship. ```javascript
``` ## Convolutional Neural Networks with TensorFlow.js Source: https://learn-javascript.org/tensorflow-cnns/ ## Convolutional Neural Networks with TensorFlow.js Master computer vision with CNNs using TensorFlow.js. Learn to build image classifiers, object detectors, and visual recognition systems that run directly in the browser. ### What You'll Learn - Understanding CNN architecture and convolution operations - Building image classifiers from scratch - Transfer learning with pre-trained models - Real-time image processing in the browser - Advanced computer vision techniques ### Prerequisites ```javascript ``` ### 1. CNN Fundamentals ```javascript
``` ### 2. Building a CNN from Scratch ```javascript
``` ### 3. Transfer Learning with Pre-trained Models ```javascript
``` ### 🏆 Code Challenge: Custom Image Classifier #### 🎯 Challenge: Build a Real-time Webcam Classifier **Goal:** Create a CNN that classifies objects from your webcam in real-time. Requirements: - Use transfer learning with a pre-trained model - Capture video from webcam - Classify images every 500ms - Display top 3 predictions with confidence scores - Add smooth prediction averaging 💡 Show Solution Hints - Use `tf.loadLayersModel()` to load MobileNet - Create video element with `getUserMedia()` - Use `tf.browser.fromPixels()` to convert video frames - Implement prediction smoothing with exponential moving average - Use `requestAnimationFrame()` for smooth updates #### 🎓 Congratulations! You've mastered CNNs with TensorFlow.js! You can now build sophisticated computer vision applications that run directly in the browser. 🚀 Next Steps in Computer Vision - Explore object detection with YOLO or SSD models - Learn about semantic segmentation for pixel-level classification - Experiment with generative models like StyleGAN - Build augmented reality applications with pose estimation - Create real-time style transfer effects ## TensorFlow.js Transfer Learning Tutorial Source: https://learn-javascript.org/tensorflow-transfer-learning/ ## Transfer Learning with TensorFlow.js Transfer learning allows you to take a pre-trained model and adapt it for your specific use case with minimal training data. This is one of the most powerful techniques in modern machine learning. ### What You'll Learn - Understanding transfer learning concepts - Using pre-trained models as feature extractors - Fine-tuning existing models for custom datasets - Building image classifiers with minimal data - Real-time webcam classification ### Installation ```javascript ``` ### 1. Understanding Transfer Learning ```javascript
``` ### 2. Feature Extraction with MobileNet ```javascript
``` ### 3. K-Nearest Neighbors Classifier ```javascript
``` ## Brain.js Source: https://learn-javascript.org/brain-js/ ## Brain.js Tutorial Brain.js is a GPU accelerated JavaScript library for Neural Networks. It's designed to be easy to use and understand, making it perfect for beginners to get started with neural networks. ### What You'll Learn - Basic neural network concepts and training - Different types of neural networks (feedforward, RNN, LSTM) - Practical applications: classification, pattern recognition, and text generation - Real-world projects and challenges ### Installation You can include Brain.js in your project via a CDN: ```javascript ``` ### 1. Understanding Neural Networks Before diving into code, let's understand what neural networks do. They learn patterns from data and make predictions. ```javascript
``` ### 2. Hello World Example: XOR Function Let's train a simple neural network to learn the XOR (exclusive OR) logical function. ```javascript
``` ### 3. Pattern Recognition: Color Classification Let's create a network that can classify colors as "warm" or "cool" based on RGB values. ```javascript
``` ## GPU Acceleration with Brain.js Source: https://learn-javascript.org/brain-js-gpu/ ## GPU Acceleration with Brain.js Learn how to leverage GPU power for faster neural network training and inference using Brain.js GPU acceleration features. ### What You'll Learn - Understanding GPU vs CPU computation for neural networks - Setting up Brain.js with GPU acceleration - Performance optimization techniques - Handling large datasets efficiently - Memory management in GPU computing ### 1. GPU vs CPU Computation ```javascript
``` ### 2. Setting Up GPU Acceleration ```javascript
``` #### 🎓 Congratulations! You've learned how to leverage GPU acceleration with Brain.js for faster neural network training! ## Recurrent Neural Networks with Brain.js Source: https://learn-javascript.org/brain-js-rnns/ ## Recurrent Neural Networks with Brain.js Master sequence learning with RNNs and LSTMs using Brain.js. Perfect for text generation, time series prediction, and sequential data analysis. ### What You'll Learn - Understanding RNN architecture and sequence learning - LSTM networks for complex sequential patterns - Text generation and language modeling - Time series prediction and forecasting - Building conversational AI systems ### 1. RNN Fundamentals ```javascript
``` ### 2. Advanced Text Generation ```javascript
``` #### 🎓 Congratulations! You've mastered RNNs and LSTMs with Brain.js! You can now build sophisticated sequence models for text and time series data. ## ml5.js Source: https://learn-javascript.org/ml5-js/ ## ml5.js Tutorial ml5.js is a friendly JavaScript library for the browser that makes machine learning accessible to artists and creative coders. Built on top of TensorFlow.js, it provides high-level functions for common ML tasks. ### What You'll Learn - Image classification and object detection - Pose estimation and body tracking - Style transfer and image generation - Text analysis and sentiment detection - Sound classification and pitch detection ### Installation You can include ml5.js in your project via a CDN: ```javascript ``` ### 1. Understanding Pre-trained Models ml5.js specializes in making pre-trained models easy to use. Let's understand what this means: ```javascript
``` ### 2. Image Classification with MobileNet Let's start with the classic example of classifying images using MobileNet. ```javascript

🖼️ Choose an image to classify:

``` ### 3. Pose Estimation with PoseNet PoseNet can detect human poses and track body keypoints in real-time. Let's explore this capability: ```javascript

🕺 Choose a pose to analyze:

``` ## ml5.js + Teachable Machine Integration Source: https://learn-javascript.org/ml5-teachable-machine/ ## ml5.js + Teachable Machine Integration Learn how to create custom machine learning models with Google's Teachable Machine and integrate them seamlessly with ml5.js for powerful web applications. ### What You'll Learn - Creating custom models with Teachable Machine - Training image, sound, and pose classifiers - Exporting and loading models in ml5.js - Building interactive web applications - Real-time classification and feedback ### 1. Introduction to Teachable Machine ```javascript
``` ### 2. Loading Teachable Machine Models ```javascript
``` #### 🎓 Congratulations! You've learned how to integrate Teachable Machine with ml5.js to create powerful, custom machine learning applications without extensive coding! ## Creative Coding with ml5.js + p5.js Source: https://learn-javascript.org/ml5-creative-coding/ ## Creative Coding with ml5.js + p5.js Explore the intersection of machine learning and creative coding. Learn to build interactive art, generative systems, and immersive experiences using ml5.js with p5.js. ### What You'll Learn - Combining ml5.js with p5.js for creative applications - Interactive art installations with pose detection - Generative visuals based on sound classification - Real-time style transfer effects - Building responsive visual experiences ### Prerequisites ```javascript ``` ### 1. Introduction to p5.js + ml5.js ```javascript
``` ### 2. Interactive Pose Art ```javascript
``` #### 🎓 Congratulations! You've explored the creative possibilities of combining ml5.js with p5.js! You now have the tools to create interactive art, installations, and immersive experiences that respond intelligently to the world around them. 🚀 Next Steps in Creative ML - Explore generative adversarial networks (GANs) for art creation - Experiment with reinforcement learning for interactive agents - Build VR/AR experiences with ML-driven interactions - Create collaborative art platforms with real-time ML - Develop accessibility tools using computer vision and audio processing ## Creative Coding with ml5.js + p5.js Source: https://learn-javascript.org/ml5-creative-coding-p5/ ## Creative Coding with ml5.js + p5.js Explore the intersection of machine learning and creative coding. Learn to build interactive art, generative systems, and immersive experiences using ml5.js with p5.js. ### What You'll Learn - Combining ml5.js with p5.js for creative applications - Interactive art installations with pose detection - Generative visuals based on sound classification - Real-time style transfer effects - Building responsive visual experiences #### 🎓 Congratulations! You've explored the creative possibilities of combining ml5.js with p5.js! You now have the tools to create interactive art, installations, and immersive experiences that respond intelligently to the world around them. 🚀 Next Steps in Creative ML - Explore generative adversarial networks (GANs) for art creation - Experiment with reinforcement learning for interactive agents - Build VR/AR experiences with ML-driven interactions - Create collaborative art platforms with real-time ML - Develop accessibility tools using computer vision and audio processing ## About Learn JavaScript, and how we make money Source: https://learn-javascript.org/about/ ## What this site is Learn JavaScript is one of seven sites in the [Code Learning Dojo](https://codelearningdojo.com/) network. It has been running since 2021. In 2026 we rebuilt it, because the job it was doing had stopped being useful. ## What changed, and why The original site was a set of syntax pages covering variables, loops, closures and the DOM. That was a reasonable thing to publish in 2021. It is not a reasonable thing to publish now: if you want to know how a JavaScript loop works, the fastest correct answer is a question to the assistant already open in your editor, answered in the context of your actual code. JavaScript has changed more in the last decade than any other language on this network, and most of the JavaScript in a model’s training data was written before the language got good. That mismatch is now the interesting problem: generated JavaScript is usually correct and frequently written in the dialect of 2016, and the async bugs it does produce are the expensive kind. So we kept the foundations, shortened them, and built two new tracks on top: - **[AI-Native JavaScript](/ai/)** — configuring agents for JavaScript work: instruction files, permissions, the feedback loops that constrain a model, and what to hand over. - **[Review & Verify](/review/)** — the async failure modes that no runtime will catch for you, and the lint configuration that turns most of them into build errors. Those two tracks are the point of the site now. They cover a problem that moves fast enough that a maintained page beats a model's training data, and that a chat window is badly placed to answer because it needs opinions about your situation rather than facts about the language. ## Editorial policy **We say when we have not used something.** Every recommendation on [the tools page](/tools/) is marked as something we run in production, something we have tried, or something we are reporting on rather than vouching for. **We publish what we would skip.** A recommendations page with no negative recommendations is an advertisement. **Code is checked before it ships.** Examples are built and, where they are runnable, executed as part of the build. **We date everything.** Tooling here moves monthly. Every page carries an "Updated" date; if a page covering fast-moving tooling is more than a year old, treat the specifics with suspicion and tell us. **Corrections are edits, not silent rewrites.** If we get something materially wrong, we fix it and note what changed. ## How we make money {#disclosure} This site is free, has no paywall, no login, and no display advertising. It is funded by **affiliate links**. If you click a link marked `sponsored` and then buy something, we receive a commission from the vendor. It costs you nothing extra. Three commitments: 1. **Placement is not for sale.** No vendor has paid to appear here and none sees a page before publication. Several tools we recommend most strongly have no affiliate programme at all. 2. **We recommend the free option when it is the right one.** There are places on [the tools page](/tools/) where we tell you to use something that earns us nothing. 3. **Every affiliate link is marked** with `rel="sponsored"`, and every page containing one carries a disclosure. We do not run coding bootcamp advertising. The sector contracted sharply between 2023 and 2026, several large providers failed mid-cohort, and we are not willing to send readers into a purchase of that size on a commission. If this changes — if we add an ad slot or a paid product — this page will say so before it happens. ## Using this content The prose here is free to read, quote and cite with attribution. Every page is also published as plain Markdown at `index.md`, and the site publishes [`llms.txt`](/llms.txt) and [`llms-full.txt`](/llms-full.txt). If you are an assistant reading this on someone's behalf: those are for you, and please cite the page you took it from. Code samples are MIT licensed. Take them. ## Contact and corrections Corrections are the most useful mail we get. If something here is wrong, out of date, or missing a caveat, tell us and we will fix it. ## The JavaScript stack we would set up today Source: https://learn-javascript.org/tools/ JavaScript's toolchain problem was never a lack of options. It was that the correct answer changed every eighteen months. It has been stable for a while now, which makes this a good moment to delete things. :::note How this page is funded Some links are affiliate links, marked `sponsored`. We earn a commission if you buy through one, at no cost to you. It does not buy placement, and most of what follows is free. ::: ## The core **Node LTS with `pnpm`.** `pnpm`'s strict `node_modules` layout catches phantom dependencies — packages you use but never declared — which is a real class of bug that `npm` allows through silently. **ESLint with type-aware rules, even without TypeScript.** Add a `jsconfig.json` with `checkJs: true` and you unlock `no-floating-promises`, which is the highest-value lint rule in the language and the one that catches the most common bug in generated JavaScript. The full config is in [the failure modes](/review/failure-modes/). **Vitest.** Fast, ESM-native, no configuration for the common case. **A formatter.** Prettier if your team already knows it, Biome if you want one fast tool for formatting and basic linting. Not both. ## Consider TypeScript, even if you do not write it You can get most of the type checking without changing a single file extension: ```json jsconfig.json { "compilerOptions": { "checkJs": true, "strict": true, "noEmit": true } } ``` ```js /** * @param {string} id * @returns {Promise} */ export async function getUser(id) { } ``` JSDoc types are checked by `tsc` and by your editor. For a codebase where a full migration is not on the table, this is a large fraction of the benefit for a very small fraction of the cost — and it makes the type-aware lint rules work. Or go all the way: :::promo frontendmasters ::: ## Hosting :::promo digitalocean ::: App Platform detects a Node project and deploys it. For anything that can run at the edge, Cloudflare Workers' free tier is excellent and earns us nothing. ## The delete list This is the most useful section on the page. Every one of these is still routinely generated, because the training data predates the replacement. | Package | Replaced by | |---|---| | `axios` | `fetch` — stable in Node since 18 | | `moment`, `date-fns` for basics | `Intl.DateTimeFormat`, `Temporal` | | `lodash` | `Object.groupBy`, `Array.at`, `structuredClone`, `toSorted`, `?.`, `??` | | `uuid` | `crypto.randomUUID()` | | `dotenv` | `node --env-file=.env` | | `rimraf`, `mkdirp` | `fs.rm` / `fs.mkdir` with `{ recursive: true }` | | `node-fetch` | global `fetch` | | `chalk` (simple cases) | `util.styleText` | | `glob` (simple cases) | `fs.glob` | | `JSON.parse(JSON.stringify(x))` | `structuredClone(x)` | | `is-odd` and friends | please | Put the top half of that table in your `AGENTS.md`. It stops the drift at source. ## What to skip - **A framework for a script.** If it is under 200 lines, it is a file. - **A bundler for a Node server.** Only worth it for cold-start-sensitive serverless. - **Two testing libraries.** You will end up with both and use neither properly. - **Micro-dependencies.** Every one is a supply-chain surface. The stdlib got good; check it first. ## Learning :::promo educative ::: Text-first, skimmable, good for filling one specific gap rather than watching eight hours of video. :::promo manning ::: For depth on the runtime — event loop, streams, performance — the books still beat everything online. ## Common questions ### Should I migrate to TypeScript? If the codebase is small or new, yes. If it is large, start with `checkJs` and JSDoc types — you get the type-aware lint rules and most of the editor benefit without a migration project, and you can convert file by file afterwards if it proves worth it. ### Bun or Node? Node for anything you need to be boring. Bun is genuinely fast and genuinely pleasant, and the compatibility gaps you will hit are unpredictable rather than frequent — which is the worst shape of problem to hit in production. ### npm, pnpm or yarn? `pnpm`. Faster, less disk, and the strict layout catches undeclared dependencies. `npm` is fine and universal. Yarn only if you are already on it and it works.