Five Modern JavaScript Features That Make the Old Patterns Look Silly
I’ve been doing some reading on what JavaScript has been picking up over the last few releases, and the current batch is unusually good. Cleaner resource management, real Set math, lazy iterators, and a couple of small ergonomic wins that retire some genuinely tedious patterns. So this post is my attempt at summarizing five of the more interesting ones, what they replace, and where each one stands on browser support. I hope it helps if you’re trying to figure out what’s actually shipping versus what’s still a proposal.
1. Explicit Resource Management with using
If you’ve written C# or Python, this will feel familiar. The using keyword (and its async sibling await using) ensures a resource is cleaned up the moment the variable goes out of scope, even if your code throws. Under the hood it looks for Symbol.dispose or Symbol.asyncDispose on the object.
The old way meant remembering to wrap everything in try/finally:
async function fetchUser() {
const db = new DatabaseConnection();
await db.connect();
try {
return await db.query('SELECT * FROM users WHERE id = 1');
} finally {
await db.close();
}
}
The new way:
async function fetchUser() {
await using db = new DatabaseConnection();
await db.connect();
return await db.query('SELECT * FROM users WHERE id = 1');
}
No finally, no forgetting to close the connection on the error path. The cleanup is guaranteed.
Browser/runtime support: Chrome 123+, Firefox 119+, Node 20.9+. Safari is still pending.
2. New Set Methods
For years, JavaScript’s Set was basically a deduplicated array with a fancy name. If you wanted actual set math, you were converting to arrays and looping. Now the operations are built in and run at engine speed.
const userRoles = new Set(['read', 'write', 'comment']);
const adminRoles = new Set(['read', 'write', 'delete', 'ban', 'comment']);
userRoles.intersection(adminRoles); // shared roles
adminRoles.difference(userRoles); // what admin has that user doesn't
userRoles.union(adminRoles); // everything, deduped
userRoles.isSubsetOf(adminRoles); // true
That’s it. That’s the whole job. No more new Set([...a].filter(x => b.has(x))) incantations. The full method set also includes symmetricDifference, isSupersetOf, and isDisjointFrom.
These shipped as part of ES2024 and have reached Baseline. Available in Chrome 122+, Safari 17+, and recent Firefox.
3. Iterator Helpers
This one is genuinely a big deal. Until now, .map() and .filter() only worked on arrays, and arrays load everything into memory. If you’re streaming a 50GB log file through a generator, calling Array.from() on it will introduce you to your operating system’s OOM killer.
Iterator helpers bring those same methods to iterators, operating lazily, one item at a time.
The old way:
function* infiniteNumbers() {
let i = 1;
while (true) yield i++;
}
const evens = [];
for (const num of infiniteNumbers()) {
if (num % 2 === 0) {
evens.push(num);
if (evens.length === 3) break;
}
}
The new way:
const result = infiniteNumbers()
.filter(n => n % 2 === 0)
.take(3)
.toArray();
// [2, 4, 6]
It only computes what take(3) needs. You can chain on an infinite sequence and it just works.
These are part of ES2025. Firefox has shipped them, Chrome is in the process of shipping in V8, and Safari’s implementation is roughly half done.
4. Map Upsert
The naming bounced around (early proposals called it emplace, then upsert), but the final landing is getOrInsert(key, default) and getOrInsertComputed(key, callback). The idea is simple: stop doing the three-step “check, default, fetch” dance every time you group data.
The old way:
const wordMap = new Map();
for (const word of words) {
const key = word[0];
if (!wordMap.has(key)) {
wordMap.set(key, []);
}
wordMap.get(key).push(word);
}
The new way:
const wordMap = new Map();
for (const word of words) {
wordMap.getOrInsert(word[0], []).push(word);
}
This is the kind of thing every codebase has a groupBy helper for. The proposal reached Stage 4 in January 2026, so it’s officially in the spec, but engine implementations are still in progress as of this writing. Worth knowing about, not yet safe to ship without a polyfill.
5. Import Attributes
As ES Modules took over, importing JSON natively became a real need. The catch is that just letting import pull in a .json file is a security problem. If the server quietly serves JavaScript instead of JSON, the engine would happily execute it as code.
Import attributes fix that by making you declare the type explicitly. If the file isn’t what you said it was, the engine refuses.
import config from './config.json' with { type: 'json' };
console.log(config.databaseHost);
No more fs.readFileSync for config, no more require hacks in otherwise-modern codebases. Just an import that’s safe by default.
If you’ve seen the assert { type: 'json' } form in older articles, that was an earlier syntax that got renamed before shipping. The current keyword is with. Available in Chrome, Edge, Firefox, and Safari since April 2025, plus Node and Deno.
The Through-Line
What stands out across these five is that each one retires a pattern that’s been written into JavaScript codebases millions of times. The try/finally cleanup. The custom groupBy helper. The Lodash imports for set operations. The for loop with a manual counter because there was no .take() on generators. The fs.readFileSync for loading a config file in an otherwise-modern ESM project.
The language is quietly absorbing the utility belt, and the code that’s left looks a lot more like what we meant to write in the first place. Sign me up.
Sources
- Explicit Resource Management — V8
- JavaScript Set methods reach Baseline — web.dev
- Iterator helpers — V8
- Map.prototype.getOrInsert — MDN
- Import attributes — MDN
I’d appreciate a follow. You can subscribe with your email below. The emails go out once a week, or you can find me on Mastodon at @[email protected].