A year ago I wrote JavaScript: The Web Never Forgets - the story of how a ten-day hack became a global standard, and how bug-for-bug backward compatibility became JavaScript’s prime directive. typeof null is still "object" because fixing it would break the web. The language can’t shed its past, because the past is load-bearing.
I thought that was the end of the story: a language frozen by its own success, dragging its 1995 birthmarks into eternity.
I was wrong. It gets better. We’ve now built machines that memorized the entire graveyard and generate fresh corpses from it on demand.
Large language models learned JavaScript the only way they could: by reading the web. And the web, as established, never forgets. Every jQuery tutorial from 2009, every XMLHttpRequest snippet from 2006, every Stack Overflow answer that solved someone’s problem in 2012 and has been accumulating upvotes ever since - all of it went into the training set. The model didn’t read the changelog. It read the distribution. And the distribution of JavaScript on the internet is old, because old code doesn’t get deleted. It gets indexed.
The Corpus Is a Time Capsule
Here’s the mechanism, and it’s almost embarrassingly simple.
An LLM doesn’t know what “current best practice” is. It knows what’s statistically common in its training data. And the training data is not a snapshot of how good developers write JavaScript today - it’s an archaeological dig through twenty-five years of accumulated sediment.
Think about what that corpus actually contains. Stack Overflow’s most-viewed JavaScript answers are disproportionately from the platform’s golden era, roughly 2009–2015, because that’s when the canonical questions got asked and answered. Those answers were correct then. Many are still accepted, still highly upvoted, still ranking first on Google - and still recommending patterns that have been superseded twice over. GitHub is full of abandoned repos, forked tutorials, and bootcamp exercises that outnumber well-maintained modern codebases by orders of magnitude. W3Schools pages that taught a generation document.getElementById and var are among the most-linked programming content in history.
The model ingested all of it, weighted by prevalence. Prevalence is not correctness. Prevalence is just age times popularity. In JavaScript - a language where nothing is ever removed and everything ever written still runs - age times popularity is a formula that systematically favors the dead. And it gets subtler than that: every tutorial that demonstrated == coercion while explaining why not to use it fed the model the wart and the warning from the same page - and statistics doesn’t guarantee which one it reproduces.
In Why AI Still Sucks at Your Job I argued the bottleneck was never intelligence but instrumentation. This is a perfect case study: the model isn’t stupid when it reaches for a 2013 idiom. It’s doing exactly what it was trained to do - reproduce the most likely continuation. The likely continuation just happens to be a fossil.
The Gallery of Resurrections
Let’s look at what actually crawls out of the grave. These are representative examples of the kind of code models still produce when you don’t pin them down - every one a pattern that had a funeral years ago.
The Callback Pyramid Rises Again
Ask for “a function that fetches user data and then their orders,” give the model no context, and you have decent odds of receiving some variation of this:
function getUserOrders(userId, callback) {
fetchUser(userId).then(function (user) {
fetchOrders(user.id).then(function (orders) {
callback(null, { user: user, orders: orders });
}).catch(function (err) {
callback(err);
});
}).catch(function (err) {
callback(err);
});
}
This is a remarkable artifact: a hybrid zombie. It wraps promises (2015) in a Node-style error-first callback (2009), nests .then() instead of chaining it, and ignores async/await - which shipped in ES2017, nearly a decade ago. No human wrote code like this at any single point in history. It’s a chimera stitched together from different geological strata of the training data, like a museum reconstructing a dinosaur with bones from three different species.
The modern version is five lines and reads like prose:
async function getUserOrders(userId) {
const user = await fetchUser(userId);
const orders = await fetchOrders(user.id);
return { user, orders };
}
The model knows this form too, of course. But “knows” and “defaults to” are different things, and the default is set by the corpus, not the calendar.
moment.js: Undead Since 2020
Ask for date formatting and there’s a real chance you’ll be told to npm install moment - a library whose own maintainers declared it a legacy project in maintenance mode in September 2020 and explicitly recommend against using it in new projects. It’s 300KB of the exact bundle bloat I ranted about in the obesity epidemic section of the original post, solving problems the platform now solves natively:
// What the model suggests (RIP 2011–2020):
const moment = require('moment');
moment(date).format('MMMM Do YYYY');
// What the platform has offered for years, for free:
new Intl.DateTimeFormat('en-US', { dateStyle: 'long' }).format(date);
moment isn’t alone - the request package (deprecated February 2020) and wholesale lodash imports for array methods native since 2009 get summoned the same way. These libraries earned their millions of mentions honestly. But mentions don’t decay when the library does. The corpus has no garbage collector.
Defensive Coding Against Browsers That No Longer Exist
My personal favorite, because it’s pure archaeology:
var xhr;
if (window.XMLHttpRequest) {
xhr = new XMLHttpRequest();
} else if (window.ActiveXObject) {
xhr = new ActiveXObject('Microsoft.XMLHTTP'); // IE6 compatibility
}
That ActiveXObject branch exists to support Internet Explorer 6 - a browser released in 2001, whose successor lineage was fully retired in June 2022. fetch has been in every browser for the better part of a decade and native in Node since version 18. Yet the incantation survives, because between 2005 and 2012 it was pasted into approximately every AJAX tutorial on Earth. The same goes for addEventListener/attachEvent feature checks and polyfills for Array.prototype.forEach.
This is code written to defend against a threat that died before some of today’s junior developers finished primary school. The model generates it with total confidence, like a soldier still guarding a border that was redrawn decades ago.
The Loop Closes
Here’s where it stops being funny.
In the original post I described how the web’s memory works: once code is out in the wild, people build on it, and bug-for-bug compatibility becomes a feature. That was a one-way ratchet - old code constrains new code, but at least new code was being written by people who read release notes.
Now watch the new loop:
- A model trained on the legacy-heavy web generates legacy-flavored JavaScript.
- That code gets committed, pushed, merged, blogged about, and answered onto forums - at a scale no human cohort ever produced. Some estimates already put a substantial share of new public code as AI-generated or AI-assisted.
- The next generation of models trains on a web that now contains that output.
- The distribution the new model learns is even more confidently skewed toward the patterns the old model resurrected.
The web’s memory used to be an archive. Now it’s a feedback loop. Researchers studying recursive training call the degenerate endpoint of this model collapse - models trained on model output progressively forget the tails of the distribution and converge on a blurry average. For JavaScript, the “blurry average” of the entire historical corpus is not modern ESM with async/await and Temporal. It’s something like jQuery-era idioms wearing a React costume.
Dead patterns used to die of natural causes: their authors retired, their tutorials slid down the search rankings, code review beat them out of new hires. Every mechanism in that list is social. Models don’t attend conferences, don’t get roasted in code review, don’t read the deprecation notice at the top of the moment.js homepage. The social immune system that used to kill bad patterns has no jurisdiction over a weights file.
And it cuts the other way too: features newer than the corpus’s center of mass are underrepresented, so models underuse them. structuredClone, AbortController, Array.at(), top-level await, the Temporal API - all real, all shipped or shipping, all statistically invisible next to a million var declarations. The model isn’t just resurrecting the dead. It’s ignoring the living.
Exorcism Is a Build Step
So what do you actually do? The answer follows directly from the workflow post: the model obeys what it can see, and trusts what you enforce. You can’t retrain the corpus, but you can stop it at the door.
Make the linter the gatekeeper, not a suggestion. ESLint with no-var, prefer-const, eqeqeq, and a plugin like eslint-plugin-unicorn (which explicitly flags legacy idioms) turns “please write modern JS” from a vibe into a hard failure. Generated code that ships var or == simply doesn’t pass make verify. The model doesn’t need to know the pattern is dead if the pipeline refuses to bury it in your repo.
Ban dead dependencies mechanically. A depcheck/npm-check step plus an explicit denylist (moment, request, left-pad-era lodash usage) in CI catches the necromancy at install time. Bonus: your bundle size will thank you, and so will the planet.
Put the current year in the context window. Your rules file should say it outright: target ES2022+, ESM only, native fetch, async/await only, no .then() chains, Intl/Temporal for dates, no compatibility code for any browser we don’t support. This feels absurdly blunt. It works precisely because it’s blunt - you’re overriding a statistical prior, and priors don’t respond to subtlety. Keep your package.json and a modern example file visible; the model imitates what’s nearby.
Review generated code like a PR from a well-read time traveler. Fluent, confident, occasionally convinced it’s 2013. The tell isn’t broken code - it’s code that works while quietly importing three hundred kilobytes of the past.
None of this is exotic. It’s the same discipline the original post asked for - ship less, ship deliberately - extended to a world where your fastest developer learned everything from the graveyard.
The Web Never Forgets. Now It Dreams.
JavaScript survived its rushed birth, the browser wars, the ES4 collapse, and the V8 monoculture by promising one thing: nothing ever breaks, because nothing is ever removed. That promise built the most backward-compatible platform in computing history - and the most complete public record of its own mistakes.
We then trained machines on that record and put them in every editor on Earth.
Backward compatibility kept JavaScript’s dead patterns runnable. LLMs made them contagious. The web never forgets - so your tooling has to do the forgetting for it.