Skip to content
JavaScript js modern 4 min read

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 ** 2 is a SyntaxError. Write (-2) ** 2 or -(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)]);
PatternBehavior
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

await only works inside async functions — 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 includes over indexOf(...) !== -1 for membership checks — it’s clearer and handles NaN.
  • Reach for async/await over raw .then() chains; reserve explicit promise combinators for concurrency.
  • Run independent async work with Promise.all (or Promise.allSettled when 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.entries with destructuring in for...of loops for the cleanest object iteration.
  • Use Object.getOwnPropertyDescriptors (not Object.assign) when cloning objects that define getters/setters.
Last updated June 1, 2026
Was this helpful?