JavaScript is one of those rare technologies that keeps redefining the center of gravity for the entire web. What matters now is not a prediction of which language to bet on — it's a clear-eyed read of the JavaScript features and idioms that have earned their place in real codebases, and the ones that are quietly becoming legacy weight.
01The core language, modernised
ES2015 (ES6) was the hinge point. Destructuring, arrow functions, template literals, let/const — these are no longer "new features", they are the baseline. If you are writing var without a specific reason, you are not writing modern JavaScript. But the language kept going, and several additions since ES2015 are just as worth internalizing.
Optional chaining and nullish coalescing. ?. and ?? arrived together and eliminated huge swathes of defensive boilerplate. user?.address?.postcode ?? 'unknown' is not just shorter — it is more honest about intent. Read it before reaching for a utility library.
Logical assignment operators. ||=, &&=, and ??= feel minor until you start using them for default-population patterns. They compound well with the above.
Structured cloning. structuredClone() is built in now — no more JSON.parse(JSON.stringify(...)) hacks for deep-copying plain objects. It handles Map, Set, Date, and circular references that the JSON trick silently breaks.
Array methods you should have in muscle memory. Array.from(), .flat(), .flatMap(), .findIndex(), .at() (negative indexing, finally). The standard library has matured significantly; reaching for Lodash for these operations is often no longer necessary.
Top-level await. In ES modules, await is legal at the top level, outside any async function. This changes how you structure module initialisation and makes certain data-loading patterns far cleaner.
Array methods you should have in muscle memory.
02Async: getting the model right
Promises and async/await are the beating heart of modern JavaScript, and the single most common source of subtle bugs. The mistake developers repeat is treating async/await as if it makes code synchronous — it makes it read synchronously while the event loop keeps running. Call two awaits sequentially when they have no dependency on each other and you have left performance on the table for no reason.
// Slow — second fetch waits for first to finish
const user = await fetchUser(id);
const prefs = await fetchPrefs(id);
// Fast — both fire together
const [user, prefs] = await Promise.all([fetchUser(id), fetchPrefs(id)]);Promise.allSettled() is the underused sibling of Promise.all(). Where all rejects on the first failure, allSettled waits for every promise and gives you a result array you can inspect — invaluable when you want partial success rather than all-or-nothing.
Error handling with async/await is also worth a moment of care. An unawaited promise that rejects will not be caught by a surrounding try/catch. Always await, or attach a .catch(). The mental model for async/await is worth working through carefully if this still catches you out.
03Modules and the import/export story
The JavaScript module system (import/export) is the native standard and has been for years. CommonJS (require) is still everywhere in Node.js codebases, but for new projects and browser-facing code, ES modules are the right default. Bundlers like Vite, Rollup, and esbuild all work with ES modules natively and tree-shake dead code more effectively as a result.
Dynamic imports — import('./heavy-module.js') — return a promise and make code-splitting trivial. Load a heavy dependency only when a user action demands it, not as part of the initial bundle.
04What to leave behind
Some JavaScript habits age poorly and are worth consciously retiring.
arguments — use rest parameters (...args) instead; they are a real array, not an array-like, and they compose cleanly with destructuring.
Callback-heavy code that predates Promises. If you are maintaining code in this pattern, modernising it in layers — wrapping callbacks in Promise constructors, then converting call sites to async/await — is entirely practical.
== over ===. Type coercion in comparisons produces surprising results in edge cases that are irritatingly real in production. The mental overhead of remembering coercion rules is never worth it. Use strict equality.
Finally, resist the impulse to reach for a framework before reaching for the platform. Native APIs — fetch, IntersectionObserver, ResizeObserver, localStorage, the Web Animations API — cover a surprising amount of ground. The Web APIs worth knowing are a genuine force multiplier before you add any dependency.
