What changed in modern JavaScript
ES2015 and later releases changed how function bodies look in production code. This third post in the functions trio shows the modern features that have become table stakes: destructured parameters, async/await, generators, and tagged templates.
Foundations: Functions — the basics Internals: Closures, this, and higher-order patterns
Destructured parameters
Object destructuring turns a single options argument into a self-documenting signature.
1// Before
2function fetchPage(url, options) {
3 const method = (options && options.method) || "GET";
4 const retries = (options && options.retries) || 3;
5 // ...
6}
7
8// After
9function fetchPage(url, { method = "GET", retries = 3 } = {}) {
10 // ...
11}
The trailing = {} is essential — without it, calling fetchPage("/users") would attempt to destructure undefined and throw.
Array destructuring works the same way and is perfect for tuple-like returns:
1function divmod(a, b) {
2 return [Math.floor(a / b), a % b];
3}
4
5const [quotient, remainder] = divmod(17, 5); // 3, 2
Renaming and nested destructuring
1function render({ user: { name, email = "n/a" }, theme = "light" }) {
2 // name is now a local variable, email defaults to "n/a"
3}
Default parameters revisited
Defaults are evaluated lazily — once per call — and can reference earlier parameters:
1function range(start, end = start + 10) {
2 return { start, end };
3}
4
5range(5); // { start: 5, end: 15 }
6range(5, 7); // { start: 5, end: 7 }
async and await
An async function always returns a Promise. Inside it, await pauses execution until the awaited promise settles, then resumes with its resolved value.
1async function loadUser(id) {
2 const response = await fetch(`/users/${id}`);
3 if (!response.ok) throw new Error(response.statusText);
4 return response.json();
5}
Compare with the same code written using raw promises:
1function loadUser(id) {
2 return fetch(`/users/${id}`)
3 .then((response) => {
4 if (!response.ok) throw new Error(response.statusText);
5 return response.json();
6 });
7}
The async/await version reads like sequential code, but is still fully asynchronous — the event loop is free between awaits.
Error handling
try / catch works exactly as it would for synchronous code:
1async function safeLoad(id) {
2 try {
3 return await loadUser(id);
4 } catch (err) {
5 console.error("load failed:", err);
6 return null;
7 }
8}
Running things in parallel
await in a loop is sequential. To run promises concurrently use Promise.all (fail-fast), Promise.allSettled (collect every outcome), Promise.race, or Promise.any:
1// Sequential — slow
2const users = [];
3for (const id of ids) users.push(await loadUser(id));
4
5// Parallel — fast
6const users = await Promise.all(ids.map(loadUser));
Warning
Forgetting
awaitis the single most common async bug. The function returns a pending promise instead of the value, and any downstream code treats it as truthy garbage. Linters such as@typescript-eslint/no-floating-promisescatch this — enable it.
Top-level await
In ES modules, await works at the top level — handy for module initialisation:
1// inside config.mjs
2const config = await fetch("/config.json").then((r) => r.json());
3export default config;
Generators
A generator function (function*) returns an iterator that you advance with .next(). Each yield pauses execution and hands a value back to the caller.
1function* counter() {
2 let n = 0;
3 while (true) yield n++;
4}
5
6const c = counter();
7c.next().value; // 0
8c.next().value; // 1
9c.next().value; // 2
Because generator results implement the iteration protocol, they slot directly into for...of, spread, and destructuring:
1function* take(iter, n) {
2 for (const value of iter) {
3 if (n-- <= 0) return;
4 yield value;
5 }
6}
7
8[...take(counter(), 5)]; // [0, 1, 2, 3, 4]
Generators are how async was originally polyfilled before the language gained native support. They remain useful for lazy sequences, parser-style state machines, and tree traversal.
Tagged template literals
A template literal preceded by a function name calls that function with the static parts and the interpolated values. It is the foundation of every “css-in-js”, “html-in-js”, and SQL-builder library:
1function html(strings, ...values) {
2 return strings.reduce((out, str, i) => {
3 const safe = values[i] == null ? "" : escape(String(values[i]));
4 return out + str + safe;
5 }, "");
6}
7
8const greeting = html`<p>Hello, ${userName}</p>`;
The standard library provides String.raw as a ready-made tag that returns the unprocessed string — invaluable for regular expressions and Windows paths.
A modern function signature, end to end
Putting the pieces together:
1async function fetchPaginated(
2 endpoint,
3 { page = 1, pageSize = 25, signal } = {},
4) {
5 const url = `${endpoint}?page=${page}&size=${pageSize}`;
6 const response = await fetch(url, { signal });
7 if (!response.ok) throw new Error(`HTTP ${response.status}`);
8 const { items, total } = await response.json();
9 return { items, total, page };
10}
Destructured parameters, default values, optional cancellation via AbortSignal, async/await, structured error handling — all the modern idioms in one short function.
Summary
- Destructured parameters with
= {}defaults make options self-documenting and safe to omit. - Default parameter expressions are evaluated lazily and can reference earlier parameters.
async/awaitis sequential-looking but fully asynchronous; usePromise.allfor concurrency.- Generators (
function*+yield) produce iterators — ideal for lazy sequences and parsers. - Tagged templates power every domain-specific string DSL in the ecosystem.
These features compose with everything from closures to modules — they are the present-day baseline for production JavaScript.










