Skip to main content

ES2017~ES2020 Key Features

We'll look at the major features introduced from ES2017 through ES2020. This period added async/await, Optional Chaining, Nullish Coalescing, and other core features of modern JavaScript.


ES2017 (ES8)

async/await

Syntax that allows writing asynchronous code in a synchronous style. (Covered in detail in Chapter 6)

// Promise approach
function fetchUser(id) {
return fetch(`/api/users/${id}`)
.then(r => r.json())
.catch(err => console.error(err));
}

// async/await approach
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
return await response.json();
} catch (err) {
console.error(err);
}
}

Object.entries() / Object.values()

const user = { name: 'Alice', age: 30, city: 'Seoul' };

// Object.entries: array of [key, value] pairs
const entries = Object.entries(user);
// [['name', 'Alice'], ['age', 30], ['city', 'Seoul']]

// Convert object to Map
const map = new Map(Object.entries(user));

// Iterate over an object
for (const [key, value] of Object.entries(user)) {
console.log(`${key}: ${value}`);
}

// Filter and create new object
const filtered = Object.fromEntries(
Object.entries(user).filter(([_, v]) => typeof v === 'string')
);
// { name: 'Alice', city: 'Seoul' }

// Object.values: values only as an array
const values = Object.values(user); // ['Alice', 30, 'Seoul']

// Calculate average
const scores = { math: 90, english: 85, science: 92 };
const avg = Object.values(scores).reduce((a, b) => a + b) / Object.values(scores).length;

String.padStart() / padEnd()

// padStart(targetLength, fillString)
'5'.padStart(3, '0'); // '005'
'42'.padStart(5, '0'); // '00042'
'hello'.padStart(10); // ' hello' (default: spaces)

// padEnd
'hello'.padEnd(10, '.'); // 'hello.....'
'100'.padEnd(8, '%'); // '100%%%%%'

// Real-world: formatting
function formatTime(hours, minutes, seconds) {
return [hours, minutes, seconds]
.map(n => String(n).padStart(2, '0'))
.join(':');
}
formatTime(9, 5, 3); // '09:05:03'
formatTime(14, 30, 0); // '14:30:00'

// Padding for number alignment
const items = [
{ id: 1, name: 'Apple' },
{ id: 42, name: 'Banana' },
{ id: 100, name: 'Cherry' }
];
items.forEach(({ id, name }) => {
console.log(`${String(id).padStart(3)}: ${name}`);
});
// 1: Apple
// 42: Banana
// 100: Cherry

Object.getOwnPropertyDescriptors()

const source = {
get name() { return 'Alice'; },
set name(value) { this._name = value; }
};

// The existing Object.assign cannot copy getters/setters
const copy1 = Object.assign({}, source);
// getters/setters are called and only the values are copied

// Complete copy with getOwnPropertyDescriptors
const copy2 = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
// Full copy including getters/setters

// Useful in mixin patterns
const mixin = (target, ...sources) => {
sources.forEach(source => {
Object.defineProperties(
target,
Object.getOwnPropertyDescriptors(source)
);
});
return target;
};

ES2018 (ES9)

Object Spread/Rest Operator

// Object spread (previously only arrays were supported)
const defaults = { theme: 'light', language: 'en', fontSize: 16 };
const userPrefs = { theme: 'dark', fontSize: 18 };

// Merge (later properties take precedence)
const settings = { ...defaults, ...userPrefs };
// { theme: 'dark', language: 'en', fontSize: 18 }

// Object rest (in destructuring)
const { theme, ...rest } = settings;
// theme: 'dark', rest: { language: 'en', fontSize: 18 }

// Real-world: Redux-style immutable update
function reducer(state = initialState, action) {
switch (action.type) {
case 'UPDATE_USER':
return {
...state,
users: state.users.map(user =>
user.id === action.id
? { ...user, ...action.updates }
: user
)
};
default:
return state;
}
}

Promise.finally()

// Always runs regardless of success or failure
async function uploadFile(file) {
showProgressBar();

try {
const result = await fetch('/api/upload', {
method: 'POST',
body: file
});
return await result.json();
} catch (err) {
showErrorMessage(err.message);
throw err;
} finally {
hideProgressBar(); // always runs
cleanupTempFiles(); // always runs
}
}

// Chaining
fetch('/api/data')
.then(r => r.json())
.then(processData)
.catch(handleError)
.finally(() => {
isLoading = false;
updateUI();
});

for-await-of

Iterating over async iterables (covered in detail in Chapter 6)

// Async API pagination
async function* getPages(url) {
let nextUrl = url;
while (nextUrl) {
const { data, next } = await fetch(nextUrl).then(r => r.json());
yield* data;
nextUrl = next;
}
}

for await (const item of getPages('/api/items')) {
process(item);
}

Regular Expression Improvements

// Named Capture Groups (ES2018)
const dateStr = '2024-03-15';
const { groups: { year, month, day } } =
dateStr.match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/);

console.log(year, month, day); // 2024 03 15

// Lookbehind Assertion
// ?<= : positive lookbehind
// ?<! : negative lookbehind
const prices = ['$100', '€200', '£300'];
const usd = prices.filter(p => /(?<=\$)\d+/.test(p)); // ['$100']

// dotAll flag (s)
const multiline = 'Hello\nWorld';
/Hello.World/.test(multiline); // false (by default . doesn't match newlines)
/Hello.World/s.test(multiline); // true (resolved with s flag)

ES2019 (ES10)

Array.flat() / flatMap()

// flat(): flatten nested arrays
const nested = [1, [2, 3], [4, [5, 6]]];
nested.flat(); // [1, 2, 3, 4, [5, 6]] - one level only
nested.flat(2); // [1, 2, 3, 4, 5, 6] - two levels
nested.flat(Infinity); // [1, 2, 3, 4, 5, 6] - fully flatten

// flatMap(): combines map + flat(1)
const sentences = ['Hello World', 'Foo Bar'];
const words = sentences.flatMap(s => s.split(' '));
// ['Hello', 'World', 'Foo', 'Bar']

// Old way (less efficient)
const words2 = sentences.map(s => s.split(' ')).flat();

// Real-world: handle nested arrays in API responses
const categories = [
{ name: 'Fruits', items: ['Apple', 'Banana'] },
{ name: 'Veggies', items: ['Carrot', 'Potato'] }
];

const allItems = categories.flatMap(cat => cat.items);
// ['Apple', 'Banana', 'Carrot', 'Potato']

// Filtering effect with empty arrays
const numbers = [1, 2, -3, 4, -5];
const positiveDoubled = numbers.flatMap(n => n > 0 ? [n * 2] : []);
// [2, 4, 8]

Object.fromEntries()

// Map → object
const map = new Map([['name', 'Alice'], ['age', 30]]);
const obj = Object.fromEntries(map);
// { name: 'Alice', age: 30 }

// [key, value] array → object
const entries = [['a', 1], ['b', 2], ['c', 3]];
Object.fromEntries(entries); // { a: 1, b: 2, c: 3 }

// Object transformation pipeline
const prices = { apple: 1000, banana: 500, cherry: 2000 };

// Apply 10% discount to all prices
const discounted = Object.fromEntries(
Object.entries(prices).map(([key, value]) => [key, value * 0.9])
);
// { apple: 900, banana: 450, cherry: 1800 }

// URLSearchParams → object
const params = new URLSearchParams('name=Alice&age=30&city=Seoul');
const paramsObj = Object.fromEntries(params);
// { name: 'Alice', age: '30', city: 'Seoul' }

String.trimStart() / trimEnd()

const str = '   Hello World   ';

str.trim(); // 'Hello World' (both sides)
str.trimStart(); // 'Hello World ' (start only)
str.trimEnd(); // ' Hello World' (end only)

// Aliases: trimLeft/trimRight (previously non-standard, now standardized)
str.trimLeft(); // same as trimStart
str.trimRight(); // same as trimEnd

// Real-world: user input processing
function parseCSV(line) {
return line.split(',').map(cell => cell.trim());
}
parseCSV('Alice , 30 , Seoul '); // ['Alice', '30', 'Seoul']

Optional catch binding

// Before: had to declare catch variable even if not used
try {
JSON.parse(invalidJson);
} catch (e) { // had to declare e even without using it
isValidJson = false;
}

// ES2019: variable can be omitted
try {
JSON.parse(invalidJson);
} catch { // can be used without e
isValidJson = false;
}

// Real-world example
function isJson(str) {
try {
JSON.parse(str);
return true;
} catch {
return false;
}
}

Array.prototype.sort Stability Guarantee

// From ES2019, stable sort is guaranteed in all JS engines
const users = [
{ name: 'Charlie', age: 30 },
{ name: 'Alice', age: 25 },
{ name: 'Bob', age: 30 },
];

// When sorting by age, items with the same age preserve original order (stable sort)
users.sort((a, b) => a.age - b.age);
// Charlie and Bob have the same age, so their original order (Charlie → Bob) is preserved

ES2020 (ES11)

Optional Chaining (?.)

Prevents null/undefined errors when accessing nested objects.

const user = {
profile: {
address: {
city: 'Seoul'
}
}
};

// Old way
const city1 = user && user.profile && user.profile.address && user.profile.address.city;

// Optional Chaining
const city2 = user?.profile?.address?.city; // 'Seoul'
const zip = user?.profile?.address?.zip; // undefined (no error)

// Method calls
user?.profile?.getFullAddress?.(); // safe even if method doesn't exist

// Array access
const firstTag = user?.tags?.[0]; // safe even if array doesn't exist

// Dynamic property
const key = 'name';
const value = user?.profile?.[key];

// Optional Chaining + Nullish Coalescing combination
const displayName = user?.profile?.name ?? 'Anonymous';
const userCity = user?.address?.city ?? 'Location not set';

// In function calls
async function fetchUser(id) {
const response = await fetch(`/api/users/${id}`);
const data = await response?.json(); // returns undefined if response is null
return data?.user ?? null;
}

Nullish Coalescing (??)

// ?? : use default value only when null or undefined
// || : use default value for any falsy value (0, '', false, null, undefined)

const count = 0;
const name = '';

// || problem
count || 10; // 10 (count is 0, which is falsy → unintended result)
name || 'default'; // 'default' (empty string is also falsy)

// ?? solution
count ?? 10; // 0 (not null/undefined so original value is kept)
name ?? 'default'; // '' (empty string is also a valid value)

// Real-world
const userSettings = {
volume: 0, // 0 is a valid value
username: '', // empty string is a valid value
timeout: null, // null should use default
debug: undefined // undefined should use default
};

const volume = userSettings.volume ?? 50; // 0
const username = userSettings.username ?? 'Guest'; // ''
const timeout = userSettings.timeout ?? 5000; // 5000
const debug = userSettings.debug ?? false; // false

// ??= (Nullish Assignment) - ES2021
userSettings.timeout ??= 5000; // null, so set to 5000
userSettings.volume ??= 50; // 0, so no change

BigInt

// Handle integers larger than Number.MAX_SAFE_INTEGER
Number.MAX_SAFE_INTEGER; // 9007199254740991

// BigInt literal: n after the number
const bigNum = 9007199254740992n;
const trillion = 1_000_000_000_000n;

// BigInt constructor
BigInt(9007199254740992);
BigInt('9007199254740992');

// Operations (only between BigInts)
const a = 100n;
const b = 200n;
a + b; // 300n
a * b; // 20000n
b / a; // 2n (decimal part discarded)
b % a; // 0n

// Cannot mix with Number
// 100n + 100; // TypeError!

// Comparison is possible
100n == 100; // true (loose equality)
100n === 100; // false (different types)
100n > 99; // true

// Real-world: cryptography, large IDs
const id = 9007199254740993n;
const timestamp = BigInt(Date.now());

function factorial(n) {
if (n <= 1n) return 1n;
return n * factorial(n - 1n);
}
factorial(100n); // enormously large number

Promise.allSettled()

// Waits for all Promises to complete (regardless of success/failure)
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/invalid').then(r => r.json()), // fails
fetch('/api/products').then(r => r.json()),
]);

// Each result includes a status
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('Success:', result.value);
} else {
console.log('Failure:', result.reason);
}
});

// Filter only successes
const successes = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);

globalThis

// Access the global object regardless of environment
// Browser: window
// Node.js: global
// Web Worker: self

// Old way (differs by environment)
const global = typeof window !== 'undefined' ? window
: typeof global !== 'undefined' ? global
: typeof self !== 'undefined' ? self
: this;

// ES2020: unified with globalThis
globalThis.setTimeout(() => {}, 0);
globalThis.fetch('/api/data');
console.log(globalThis === window); // true in browser

// Detect polyfill environment
if (typeof globalThis.Proxy === 'undefined') {
// add Proxy polyfill
}

String.matchAll()

// Returns an iterator of all regex matches (requires g flag)
const text = '2024-01-15, 2024-06-20, 2024-12-31';
const dateRegex = /(\d{4})-(\d{2})-(\d{2})/g; // g flag required

// Old exec approach (repetitive)
let match;
const dates = [];
while ((match = dateRegex.exec(text)) !== null) {
dates.push({ full: match[0], year: match[1], month: match[2], day: match[3] });
}

// matchAll approach (concise)
const allMatches = [...text.matchAll(dateRegex)];
const parsedDates = allMatches.map(match => ({
full: match[0],
year: match[1],
month: match[2],
day: match[3],
index: match.index
}));

// Combined with Named Groups
const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;
for (const match of text.matchAll(namedRegex)) {
const { year, month, day } = match.groups;
console.log(`${year}-${month}-${day}`);
}

Version Summary

FeatureVersionDescription
async/awaitES2017Async syntax revolution
Object.entries/valuesES2017Convenient object iteration
String.padStart/padEndES2017String padding
Object spread/restES2018Object merging/destructuring
Promise.finallyES2018Cleanup operations
for await...ofES2018Async iteration
Array.flat/flatMapES2019Nested array processing
Object.fromEntriesES2019Entries → object conversion
String.trim{Start,End}ES2019Whitespace removal
Optional catch bindingES2019Omit catch variable
Optional Chaining ?.ES2020Safe property access
Nullish Coalescing ??ES2020Null default value
BigIntES2020Large integers
Promise.allSettledES2020Wait for all to complete
globalThisES2020Unified global object
String.matchAllES2020All regex matches

Expert Tips

1. Short-circuit evaluation with Optional Chaining

// Optional Chaining uses short-circuit evaluation
// If user is null in user?.profile, profile is not evaluated
const result = user?.profile?.expensiveMethod();
// If user is null, expensiveMethod() itself is never called

// Function arguments are also not evaluated
user?.log(expensiveCalculation());
// If user is null, expensiveCalculation() is also not executed

2. Nullish Coalescing Assignment chain

// Apply multiple default values in sequence
const config = {};

config.timeout ??= 5000;
config.retries ??= 3;
config.baseUrl ??= 'https://api.example.com';

// Or combine with Object.assign
const defaults = { timeout: 5000, retries: 3 };
const merged = { ...defaults, ...config };

3. BigInt serialization caution

// BigInt cannot be used with JSON.stringify
JSON.stringify({ id: 100n }); // TypeError!

// Solution: toJSON or replacer
const obj = { id: 100n };
JSON.stringify(obj, (key, value) =>
typeof value === 'bigint' ? value.toString() : value
);
// '{"id":"100"}'