Idiomatic modern JS
The ten patterns that separate "I know the language" from "I write modern JS". Immutable-by-default, async/await over .then, destructuring everywhere, types via TS, no var.
1 · const by default; never var
const first; let only when you reassign. var is a relic — function-scoped, hoisted, no good reason to use.
2 · Destructure in signatures
// Old
function createUser(name, age, role, country) { /* ... */ }
// Modern
function createUser({ name, age, role = "user", country = "US" }) { /* ... */ }3 · Array methods over loops
// Less idiomatic
const out = [];
for (const x of items) {
if (x.active) out.push(x.name);
}
// More idiomatic
const out2 = items.filter(x => x.active).map(x => x.name);4 · async/await over .then
// Less idiomatic
function load() {
return fetch("/api/x")
.then(r => r.json())
.then(data => {
return process(data);
})
.catch(err => console.error(err));
}
// More idiomatic
async function load() {
try {
const r = await fetch("/api/x");
const data = await r.json();
return process(data);
} catch (err) {
console.error(err);
}
}5 · ?. and ??
user?.profile?.name ?? "Anonymous" replaces five lines of guards.
6 · Spread for immutability
// Don't mutate
const u2 = { ...u, role: "admin" };
const arr2 = [...arr, newItem];
// And for setting nested fields immutably
const updated = {
...state,
user: { ...state.user, name: "Alice" }
};7 · Use Map for non-string keys
Plain objects coerce keys to strings and have prototype pollution. Map is the right tool when keys aren't compile-time-known strings.
8 · Throw Errors, not strings
// Bad
throw "bad input";
// Good — stack trace and the type system works
throw new Error("bad input");
throw new TypeError("expected number");
class ValidationError extends Error {}
throw new ValidationError("missing field");9 · TypeScript at the boundary
For new projects in 2026, default to TypeScript. The compile-time guarantees pay for themselves in a week. Use JSDoc + checkJs for legacy JS that you don't want to migrate yet.
10 · Prefer pure functions
Functions that take inputs and return outputs (no mutation, no I/O) are testable and composable. Push side effects (writes, I/O, randomness) to the edges. The middle of your app stays pure.
When it really clicks
- You write modern idioms automatically and recognise old code as old.
- You design APIs that exploit destructuring, defaults, and discriminated unions.
- You instinctively reach for TypeScript at every API boundary.