Skip to content
JavaScript js modern 4 min read

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

FeatureVersionUse it for
Object rest/spreadES2018Cloning, merging, and omitting keys
for await...ofES2018Consuming async iterables and streams
Promise.finallyES2018Settlement-agnostic cleanup
Named capture groupsES2018Readable, self-documenting regex
flat / flatMapES2019Flattening and expand-mapping arrays
Object.fromEntriesES2019Rebuilding objects from pairs/Maps
trimStart / trimEndES2019One-sided whitespace trimming
Optional catch bindingES2019catch blocks that ignore the error

Best Practices

  • Use object spread { ...a, ...b } for shallow merges, but reach for structuredClone() when you need a deep copy.
  • Prefer for await...of over manual promise chaining when consuming streams or async generators.
  • Always attach cleanup logic with Promise.finally instead of duplicating it in both then and catch.
  • Pick named capture groups over positional indexes — they survive regex edits and read clearly.
  • Reach for flatMap instead of map().flat(); it is one pass and signals intent.
  • Combine Object.entries with Object.fromEntries to filter or transform objects functionally.
  • Drop the catch binding when you do not use the error, but keep it whenever you log or inspect the failure.
Last updated June 1, 2026
Was this helpful?