JAVASCRIPT / PROJECTS AND PROFESSIONAL CRAFT
Where to go next as a JavaScript developer
Turn what next into a checklist by sorting your gaps into language, host and tooling layers, then verify features by probing the runtime you ship to.
What you will learn
- Sort any gap into language, host, or tooling and predict what survives a runtime swap
- Probe a runtime with typeof instead of trusting an edition table
- Turn learn more JavaScript into a named list of ES2020-ES2025 features you can run
- Pick one direction for the next months: browser platform, server runtime, or tooling
Understanding Where to go next as a JavaScript developer
JavaScript is three stacked things, and most of the paralysis around what to study next comes from not knowing which one you are standing in. There is the language itself, ECMAScript, which ships one edition a year and is a finite published list: closures, prototypes, iterators, generators, proxies, promises, modules. Around it sits a host that supplies effects, and hosts are swappable: a browser hands you the DOM, fetch, storage and workers, while Node, Deno and Bun hand you files, sockets and a process object. Outside both sits tooling, which nobody standardised at all: bundlers, source maps, package resolution, type checking.
That split makes a vague goal measurable. Get better at JavaScript has no end condition, but the ES2021 through ES2025 additions I have never used is a list you can write out in ten minutes and shorten by one item a week. The loop that works at this stage is not another walkthrough: pick one named capability, read its primary source, which is the TC39 proposal for a language feature or the runtime's own docs for a host API, then write a ten-line file that prints a result. Depth pays more than breadth here, because understanding why await resumes in a microtask explains a dozen bugs, while having heard of a dozen frameworks explains none.
Knowledge in this ecosystem carries a date, so get in the habit of asking the runtime instead of asking a table: typeof Object.groupBy === 'function' is true or false in the environment your users actually have, whereas an edition number only records when the committee finished the text. Then commit to one direction for a few months, browser platform depth, server-side runtimes, or the type and tooling layer, because a shallow pass over all three leaves nothing finished, and a framework is not a fourth direction, it is one opinion about the platform. Reading the source of a small library you already depend on is the cheapest jump from writing JavaScript to designing it.
// Features the language itself added, by the edition that standardised them.
const byEdition = {
ES2020: ["optional chaining", "nullish coalescing", "Promise.allSettled"],
ES2021: ["String.prototype.replaceAll", "Promise.any", "logical assignment"],
ES2022: ["top-level await", "private #fields", "Object.hasOwn"],
ES2023: ["Array.prototype.findLast", "Array.prototype.toSorted"],
ES2024: ["Object.groupBy", "Promise.withResolvers"],
ES2025: ["Set.prototype.union", "Iterator.prototype.take", "Promise.try"]
};
// Only what you can already reach for without looking it up.
const known = new Set([
"optional chaining",
"nullish coalescing",
"Promise.allSettled",
"String.prototype.replaceAll",
"private #fields"
]);
let startHere = null;
for (const [edition, features] of Object.entries(byEdition)) {
const todo = features.filter((f) => !known.has(f));
if (todo.length > 0 && startHere === null) startHere = edition;
console.log(edition + ": " + (todo.length > 0 ? todo.join(", ") : "nothing left"));
}
console.log("start here: " + startHere);JavaScript is a yearly-versioned language wrapped in a swappable host, so choosing what to learn next is really choosing which layer the gap is in: language, host, or tooling.
Worked examples
Ask the runtime, not the calendar
Shows that a feature's presence is a property of the environment you are in, and that language features and host globals answer differently.
// Run with Node 22.
const probes = {
"Object.groupBy (ES2024, language)": typeof Object.groupBy === "function",
"Set.prototype.union (ES2025, language)": typeof Set.prototype.union === "function",
"document (DOM, browser host)": typeof document !== "undefined",
"process (Node host)": typeof process !== "undefined"
};
for (const [feature, present] of Object.entries(probes)) {
console.log((present ? "yes " : "no ") + feature);
}Example explained
Line 1typeof Object.groupBy inspects the object in front of you, so it cannot be stale the way a compatibility table can.
Line 2Set.prototype.union is read, not called, so no Set instance is needed, and reading a missing property yields undefined rather than throwing.
Line 3The first two lines are engine features and arrive with a V8 upgrade in any host; the last two are host globals, so the same file in a browser flips both answers.
Line 4typeof is the only safe check for a possibly undeclared global, because a bare document reference would throw a ReferenceError in Node.
A next step in the language: make your own type iterable
Demonstrates three post-track language features at once: private fields, a generator method, and the iteration protocol that for...of consumes.
class Playlist {
#tracks = ["intro", "verse", "chorus"];
*[Symbol.iterator]() {
for (const track of this.#tracks) yield track.toUpperCase();
}
}
for (const track of new Playlist()) console.log(track);Example explained
Line 1#tracks is a real private field enforced by the language: touching it from outside the class body is a SyntaxError, not a naming convention.
Line 2*[Symbol.iterator]() is a generator stored under a computed well-known key, which is exactly the hook for...of and spread look up.
Line 3yield suspends the generator and returns one value to the loop, so INTRO is printed before verse has been uppercased.
Line 4Nothing here is browser-specific, so the same class works unchanged in Node, a worker, or a bundled page.
A next step on the platform: cancellation
Shows the signal-based cancellation pattern the platform standardised, which the four track projects never needed but any real network code does.
function wait(ms, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve("finished"), ms);
signal.addEventListener("abort", () => {
clearTimeout(id);
reject(signal.reason);
});
});
}
const controller = new AbortController();
wait(1000, controller.signal).catch((err) =>
console.log("stopped:", err.name, "-", err.message)
);
controller.abort(new Error("user navigated away"));Example explained
Line 1AbortController is a platform API rather than a language feature, but Node implements it too, which is why this file runs in both without changes.
Line 2wait only ever sees the signal, never the caller, so the same helper composes with fetch, a database driver, or another timer.
Line 3abort(reason) dispatches the abort event synchronously, so clearTimeout runs and the promise rejects long before the timer would resolve.
Line 4Rejecting with an Error you constructed keeps the printed message stable, since the default abort reason text differs between runtimes.
Important notes
An edition name records when TC39 standardised a feature, not when your target runtime shipped it; those are two different lookups and only the second decides whether your code runs.
Anything below stage 4 can still change shape: the grouping proposal moved from Array.prototype.groupBy to Object.groupBy before it landed, so keep pre-stage-4 features behind a wrapper you can delete in one place.
Common mistakes
Answering what next with a framework name: you become fluent in one library's API, and when that API changes or the stack changes you discover you never learned the DOM or the language underneath, so nothing transfers.
Reading edition names as support levels: calling toSorted because ES2023 sounds old produces TypeError: x.toSorted is not a function on an older webview or an LTS runtime you forgot was in the pipeline.
Queueing tutorials instead of writing files: you will recognise a Proxy or a generator in someone else's code but never reach for one in your own, because recognition and recall are trained separately.
Try it yourself
Change, predict, then run
In a browser console, print a yes/no probe for Object.groupBy, Iterator.prototype.take, structuredClone and process. Then write one sentence naming which of the four could never appear in a browser and which could show up with the next engine update, and say why.
Open the JavaScript workspaceCheck your understanding
A formatting module you wrote for the browser uses Intl.NumberFormat, Object.groupBy and document.createElement. You want to reuse it in a Node script. Which part is the real obstacle, and why?
- document.createElement, because the DOM is a host API rather than part of the language, so Node has no document at all
- Object.groupBy, because ES2024 additions are browser-only until Node adopts them separately
- Intl.NumberFormat, because locale-aware formatting is provided by the browser and Node cannot format numbers
- All three, because Node and browsers implement different languages
Show answer
Object.groupBy and Intl.NumberFormat are specified on the language side and ship with the engine and its ICU data, so Node gets them by upgrading V8, not by copying browsers. document comes from the DOM specification, which Node never implemented, so that call can only be replaced, not waited for. Option 1 is tempting because recent features feel browser-first, but engine features reach Node within a release or two, while a host API never arrives.