ES2017: async/await & more
After the massive ES2015 (ES6) release, the TC39 committee switched to a yearly cadence of smaller, focused additions. ES2016 was deliberately tiny — just two features — while ES2017 delivered one of the most impactful syntax additions in the language’s history: async/await. This page walks through both releases, from the humble exponentiation operator to ergonomic asynchronous code that reads like it’s synchronous.
ES2016: a deliberately small release
ES2016 shipped only two features, proving the new annual process could release on time without cramming. Both are small quality-of-life wins you’ll reach for constantly.
Array.prototype.includes
Before ES2016, checking whether an array contained a value meant indexOf(x) !== -1, which is awkward and fails on NaN. Array.prototype.includes returns a clean boolean and uses the SameValueZero algorithm, so it correctly finds NaN.
const ids = [10, 20, 30, NaN];
console.log(ids.includes(20)); // true
console.log(ids.includes(99)); // false
console.log(ids.includes(NaN)); // true ← indexOf can't do this
// indexOf comparison
console.log([NaN].indexOf(NaN)); // -1 (uses strict equality)
// optional fromIndex second argument
console.log(ids.includes(10, 1)); // false (start searching at index 1)
Output:
true
false
true
-1
false
The exponentiation operator (**)
The ** operator is shorthand for Math.pow, and it’s right-associative like exponentiation in mathematics.
console.log(2 ** 10); // 1024
console.log(Math.pow(2, 10)); // 1024 (equivalent)
console.log(2 ** 3 ** 2); // 512 — right-associative: 2 ** (3 ** 2)
let n = 3;
n **= 4; // exponentiation assignment
console.log(n); // 81
Output:
1024
1024
512
81
Operands of
**cannot have a unary operator directly on the left without parentheses:-2 ** 2is a SyntaxError. Write(-2) ** 2or-(2 ** 2)to make intent explicit.
ES2017: async/await
A Promise-returning function can be marked async, which lets you await other promises inside it. await pauses the function until the promise settles, then resumes with the resolved value — turning nested .then() chains into flat, readable code. An async function always returns a promise, and a thrown error rejects that promise.
async function getUser(id) {
const res = await fetch(`https://api.example.com/users/${id}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json(); // resolved value of the returned promise
}
async function main() {
try {
const user = await getUser(42);
console.log(user.name);
} catch (err) {
console.error('Failed to load user:', err.message);
}
}
main();
Errors are handled with ordinary try/catch, and you can run independent awaits concurrently with Promise.all instead of awaiting them one after another.
// Sequential (slow): each await waits for the previous one
const a = await getUser(1);
const b = await getUser(2);
// Concurrent (fast): both requests start immediately
const [c, d] = await Promise.all([getUser(1), getUser(2)]);
| Pattern | Behavior |
|---|---|
await x; await y; | Runs x then y in sequence |
await Promise.all([x, y]) | Runs both concurrently, fails fast |
await Promise.allSettled([x, y]) | Runs both concurrently, never short-circuits |
awaitonly works insideasyncfunctions — or at the top level of an ES module (top-level await, ES2022). Using it in a regular script’s top level is a SyntaxError.
Object.entries and Object.values
These complement the older Object.keys. Object.values returns an array of an object’s own enumerable string-keyed property values; Object.entries returns [key, value] pairs — perfect for iterating with for...of or building a Map.
const scores = { math: 90, science: 85, art: 78 };
console.log(Object.values(scores)); // [90, 85, 78]
console.log(Object.entries(scores)); // [['math', 90], ['science', 85], ['art', 78]]
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}
// Round-trip an object into a Map and back
const asMap = new Map(Object.entries(scores));
const back = Object.fromEntries(asMap); // Object.fromEntries is ES2019
console.log(back);
Output:
[ 90, 85, 78 ]
[ [ 'math', 90 ], [ 'science', 85 ], [ 'art', 78 ] ]
math: 90
science: 85
art: 78
{ math: 90, science: 85, art: 78 }
String padding: padStart and padEnd
padStart and padEnd pad a string to a target length, repeating a fill string (default a space). They’re ideal for aligning console output, zero-padding numbers, and formatting time.
console.log('5'.padStart(3, '0')); // '005'
console.log('42'.padEnd(6, '.')); // '42....'
console.log(String(7).padStart(2, '0')); // '07'
// Align a price column
const items = [['Coffee', 4], ['Sandwich', 12], ['Tea', 3]];
for (const [name, price] of items) {
console.log(name.padEnd(10) + `$${price}`.padStart(5));
}
Output:
005
42....
07
Coffee $4
Sandwich $12
Tea $3
Object.getOwnPropertyDescriptors
Object.getOwnPropertyDescriptor (singular) already existed; ES2017 added the plural form returning descriptors for all own properties at once. The key use case is correctly copying objects that include getters/setters — something Object.assign cannot do because it invokes getters and copies the resulting value, not the accessor itself.
const source = {
_temp: 20,
get celsius() { return this._temp; },
set celsius(v) { this._temp = v; },
};
// Object.assign would flatten the getter into a plain data property.
const clone = Object.create(
Object.getPrototypeOf(source),
Object.getOwnPropertyDescriptors(source)
);
clone.celsius = 30;
console.log(clone.celsius); // 30
console.log(Object.getOwnPropertyDescriptor(clone, 'celsius').get); // [Function: get celsius]
Output:
30
[Function: get celsius]
Best Practices
- Prefer
includesoverindexOf(...) !== -1for membership checks — it’s clearer and handlesNaN. - Reach for
async/awaitover raw.then()chains; reserve explicit promise combinators for concurrency. - Run independent async work with
Promise.all(orPromise.allSettledwhen you need every result regardless of failures) instead of awaiting sequentially. - Always wrap awaited calls that can fail in
try/catch, or attach a.catch()to the top-level call so rejections aren’t swallowed. - Use
Object.entrieswith destructuring infor...ofloops for the cleanest object iteration. - Use
Object.getOwnPropertyDescriptors(notObject.assign) when cloning objects that define getters/setters.