JavaScript Techniques That Actually Hold Up in 2026

The JavaScript techniques worth keeping in 2026 are the boring ones: optional chaining and nullish coalescing for safe defaults, structured async with Promise.all and clear error handling, plus native array methods over manual loops. They cut bugs and keep code readable. Skip the syntax tricks nobody maintains.
Part of the complete web development guide.
On this page
- Use optional chaining and nullish coalescing, but know the difference
- Stop fighting async. Structure it.
- Parallel work with Promise.all
- Don’t await inside a loop unless you mean to
- Real concurrency with Web Workers
- Prefer structuredClone over JSON hacks
- Array methods that replace whole loops
- Handle errors like you mean it
- Type safety without the ceremony
- Where this fits in real projects
- FAQ
- Is optional chaining slower than checking manually?
- Should I use Web Workers for every async task?
- Do I still need a build step in 2026?
Most “ultimate JavaScript techniques” lists are just a pile of syntax features with no context about when they help. I’ve been shipping JavaScript for over a decade, and the techniques that matter aren’t the flashy ones. They’re the ones that make code easier to read, faster to run, and less likely to break at 2am.
Here’s what I actually reach for in 2026, with real code and honest notes on the tradeoffs.
Use optional chaining and nullish coalescing, but know the difference
These two operators are old news by now, but people still mix them up. Optional chaining (?.) short-circuits when something is null or undefined. Nullish coalescing (??) only falls back on null or undefined, not on falsy values like 0 or "".
const config = {
retries: 0,
label: "",
};
// Wrong: 0 and "" are falsy, so you lose real values
const retries = config.retries || 3; // 3, oops
const label = config.label || "default"; // "default", also wrong
// Right: only null/undefined trigger the fallback
const retries = config.retries ?? 3; // 0
const label = config.label ?? "default"; // ""
// Chaining deep into possibly-missing objects
const city = user?.address?.city ?? "unknown";
If you’ve been using || for defaults and wondering why a zero or empty string keeps getting overwritten, this is why.
Stop fighting async. Structure it.
Most async bugs aren’t about promises being hard. They’re about not thinking through ordering and error handling. Three patterns cover almost everything I do.
Parallel work with Promise.all
// Fires all three at once, waits for all
const [user, posts, settings] = await Promise.all([
fetchUser(id),
fetchPosts(id),
fetchSettings(id),
]);
Use Promise.all when you need every result and a single failure should stop everything. Use Promise.allSettled when you want partial results even if one request fails. That distinction saves a lot of grief when you’re calling third-party APIs that go down.
const results = await Promise.allSettled([
fetchUser(id),
fetchPosts(id),
]);
for (const result of results) {
if (result.status === "fulfilled") {
console.log(result.value);
} else {
console.error(result.reason);
}
}
Don’t await inside a loop unless you mean to
// Sequential: each request waits for the last. Slow.
for (const id of ids) {
await process(id);
}
// Parallel: all at once. Fast, but be careful with rate limits.
await Promise.all(ids.map((id) => process(id)));
If you’re hitting an API with rate limits, sequential is the safer choice. If you’re doing CPU-bound work, parallel doesn’t help in JavaScript anyway because it’s single-threaded. That’s what workers are for.
Real concurrency with Web Workers
JavaScript runs on one thread. Heavy computation blocks the UI, and no amount of clever async code fixes that. For anything CPU-heavy (parsing large files, image processing, complex calculations), move it off the main thread.
// worker.js
self.onmessage = (event) => {
const { data } = event;
const result = heavyCalculation(data);
self.postMessage(result);
};
// main.js
const worker = new Worker("worker.js", { type: "module" });
worker.postMessage(bigArray);
worker.onmessage = (event) => {
console.log("Result:", event.data);
worker.terminate();
};
Module workers are supported across all current browsers now, so you can import inside the worker file. This is the single biggest performance win available for a lot of apps, and it’s still underused.
Prefer structuredClone over JSON hacks
For years people deep-cloned objects with JSON.parse(JSON.stringify(obj)). It works until it doesn’t: it drops undefined, breaks on Date, chokes on circular references, and mangles Map and Set.
const original = {
date: new Date(),
map: new Map([["a", 1]]),
nested: { count: 5 },
};
// The old hack loses the Date and the Map
const bad = JSON.parse(JSON.stringify(original));
// structuredClone handles all of it
const good = structuredClone(original);
structuredClone is built into every modern runtime. There’s almost no reason to use the JSON trick anymore.
Array methods that replace whole loops
Chaining array methods makes intent obvious. A few that people forget exist:
flatMapmaps and flattens in one pass, perfect for expanding nested data.at(-1)grabs the last item withoutarr[arr.length - 1].findLastandfindLastIndexsearch from the end.Object.groupBy(now standard) replaces a lot of manual reduce code.
const orders = [
{ id: 1, status: "shipped" },
{ id: 2, status: "pending" },
{ id: 3, status: "shipped" },
];
const grouped = Object.groupBy(orders, (o) => o.status);
// { shipped: [...], pending: [...] }
const last = orders.at(-1);
The one caveat: don’t chain six methods in a hot loop and then wonder why it’s slow. Each method allocates a new array. For tight performance-critical paths, a plain for loop still wins.
Handle errors like you mean it
Silent failures are the worst kind. Two habits that pay off.
First, always catch what you can act on and let the rest bubble up. Don’t wrap everything in a try/catch that just logs and continues.
async function loadData() {
try {
return await fetch("/api/data").then((r) => {
if (!r.ok) throw new Error(`HTTP ${r.status}`);
return r.json();
});
} catch (error) {
// Only catch if you can do something about it
reportToMonitoring(error);
throw error; // Let the caller decide
}
}
Second, use AggregateError when multiple things can fail at once. Promise.any throws one if every promise rejects, and it’s a clean way to model “try these fallbacks.”
Type safety without the ceremony
Most teams I work with in 2026 are on TypeScript, and for good reason. But if you’re staying in plain JavaScript, JSDoc type annotations plus a checkJs setting in your config gets you a surprising amount of the benefit with zero build step.
/**
* @param {string} id
* @param {{ retries?: number }} [options]
* @returns {Promise<User>}
*/
async function getUser(id, options = {}) {
// ...
}
Editors pick this up automatically, and you get autocomplete and error checking without a compiler in the pipeline. It’s a good middle ground for smaller projects or legacy codebases you can’t fully convert.
Where this fits in real projects
None of these techniques are exotic. The point isn’t to use every feature in the language. It’s to reach for the right one at the right time: ?? instead of || when zero is valid, Promise.allSettled when partial results matter, a worker when the main thread is choking. That judgment is what separates code that ships from code that breaks. When I’m doing custom web development for clients, these are the decisions that come up on nearly every project, and getting them right early saves rewrites later.
Pick one thing from this list you’re not doing yet and try it on your next task. That beats reading fifty more tips you’ll never apply.
FAQ
Is optional chaining slower than checking manually?
In practice, no. Modern JavaScript engines optimize it well, and the readability gain is worth far more than any theoretical micro-difference. Only worry about it in a measured hot path.
Should I use Web Workers for every async task?
No. Workers are for CPU-bound work that blocks the main thread. For network requests and I/O, regular async code is already non-blocking and adding a worker just complicates things.
Do I still need a build step in 2026?
Less than you used to. Native ES modules, module workers, and JSDoc-based type checking cover a lot of ground. You still want a bundler for large apps and a compiler if you’re using TypeScript, but small projects can ship straight to the browser.


