ES2018–2019 Features
ES2018 and ES2019 were two pragmatic releases that smoothed over everyday friction in working with objects, arrays, asynchronous streams, and strings. None of these features are flashy, but together they removed a lot of boilerplate that previously required helper libraries like Lodash. This page walks through each addition with runnable examples so you can apply them in modern browsers and Node.js immediately.
ES2018: object rest and spread
ES2015 gave us rest/spread for arrays; ES2018 extended the same ... syntax to object literals and destructuring. Spread copies enumerable own properties into a new object, while rest collects the “remaining” properties into a single object.
const user = { id: 1, name: "Ada", role: "admin", active: true };
// Spread: shallow-merge / clone
const updated = { ...user, role: "owner" };
// Rest: pull off some keys, keep the rest
const { id, ...profile } = user;
console.log(updated.role);
console.log(profile);
Output:
owner
{ name: 'Ada', role: 'admin', active: true }
Object spread is a shallow copy. Nested objects and arrays are shared by reference between the original and the copy, so mutating them affects both.
Gotcha: Spread copies only enumerable own properties — it ignores prototype methods and non-enumerable properties. Later keys win on conflict, so
{ ...defaults, ...overrides }is the idiomatic way to apply overrides.
ES2018: asynchronous iteration
The for await...of loop iterates over async iterables — objects that produce promises one at a time. This is the natural way to consume async generators and streaming sources without manually chaining .next() calls.
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url);
yield res.json();
}
}
async function run() {
for await (const page of fetchPages(["/a.json", "/b.json"])) {
console.log(page.title);
}
}
In Node.js, readable streams are async iterables, so you can read a file line by line or chunk by chunk with the same for await...of construct.
ES2018: Promise.finally
Promise.prototype.finally() runs a callback when a promise settles, regardless of whether it fulfilled or rejected. It is ideal for cleanup such as hiding a spinner, and it passes the original value or rejection straight through.
function load(url) {
showSpinner();
return fetch(url)
.then((res) => res.json())
.finally(() => hideSpinner());
}
The finally callback receives no arguments and does not change the resolved value (unless it throws or returns a rejected promise).
ES2018: named capture groups
Regular expressions gained named capture groups with the (?<name>...) syntax, plus the s (dotAll) flag and lookbehind assertions. Named groups make matches self-documenting via the groups object.
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups } = re.exec("2026-06-01");
console.log(`${groups.day}/${groups.month}/${groups.year}`);
Output:
01/06/2026
ES2019: Array.prototype.flat and flatMap
flat() flattens nested arrays up to a given depth (default 1), and flatMap() maps then flattens one level in a single, efficient pass.
const nested = [1, [2, [3, [4]]]];
console.log(nested.flat()); // depth 1
console.log(nested.flat(Infinity)); // fully flat
const words = ["hello world", "foo bar"];
console.log(words.flatMap((s) => s.split(" ")));
Output:
[ 1, 2, [ 3, [ 4 ] ] ]
[ 1, 2, 3, 4 ]
[ 'hello', 'world', 'foo', 'bar' ]
flatMap is handy for “map that sometimes produces zero or many items” — return [] to drop an element or a multi-element array to expand it.
ES2019: Object.fromEntries
Object.fromEntries() is the inverse of Object.entries(). It builds an object from any iterable of [key, value] pairs — including a Map, which makes round-tripping and transforming objects clean.
const prices = { apple: 1, banana: 2, cherry: 3 };
// Transform values, then rebuild the object
const doubled = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 2])
);
console.log(doubled);
// Convert a Map to a plain object
const map = new Map([["a", 1], ["b", 2]]);
console.log(Object.fromEntries(map));
Output:
{ apple: 2, banana: 4, cherry: 6 }
{ a: 1, b: 2 }
ES2019: string trimming and optional catch binding
ES2019 standardized trimStart() and trimEnd() (aliasing the older trimLeft/trimRight), and made the catch binding optional when you do not need the error object.
const raw = " hello ";
console.log(`[${raw.trimStart()}]`);
console.log(`[${raw.trimEnd()}]`);
function isValidJSON(text) {
try {
JSON.parse(text);
return true;
} catch { // no binding needed
return false;
}
}
console.log(isValidJSON("{bad}"));
Output:
[hello ]
[ hello]
false
Feature summary
| Feature | Version | Use it for |
|---|---|---|
| Object rest/spread | ES2018 | Cloning, merging, and omitting keys |
for await...of | ES2018 | Consuming async iterables and streams |
Promise.finally | ES2018 | Settlement-agnostic cleanup |
| Named capture groups | ES2018 | Readable, self-documenting regex |
flat / flatMap | ES2019 | Flattening and expand-mapping arrays |
Object.fromEntries | ES2019 | Rebuilding objects from pairs/Maps |
trimStart / trimEnd | ES2019 | One-sided whitespace trimming |
| Optional catch binding | ES2019 | catch blocks that ignore the error |
Best Practices
- Use object spread
{ ...a, ...b }for shallow merges, but reach forstructuredClone()when you need a deep copy. - Prefer
for await...ofover manual promise chaining when consuming streams or async generators. - Always attach cleanup logic with
Promise.finallyinstead of duplicating it in boththenandcatch. - Pick named capture groups over positional indexes — they survive regex edits and read clearly.
- Reach for
flatMapinstead ofmap().flat(); it is one pass and signals intent. - Combine
Object.entrieswithObject.fromEntriesto filter or transform objects functionally. - Drop the
catchbinding when you do not use the error, but keep it whenever you log or inspect the failure.