Skip to main content

What ES6 brought for async code

The main thing ES6 (2015) brought for asynchrony is Promise: an object that represents the result of an asynchronous operation, a result that may be obtained now, later or never. Along with it came .then(), .catch(), call chains and the static methods Promise.all and Promise.race, which removed nested callbacks from application code.

Theory

TL;DR

  • Before ES6, asynchrony was built on callbacks, and deep nesting produced callback hell.
  • ES6 introduced Promise: a "promise" to return a result once the operation finishes.
  • .then() handles success, .catch() catches errors in one place.
  • Chains of .then() make the code linear: the result of one step flows into the next.
  • ES6 also gave us Promise.resolve, Promise.reject, Promise.all and Promise.race.
  • async/await is ES2017, syntactic sugar on top of promises rather than a replacement for them.

Quick example

javascript
const promise = new Promise((resolve, reject) => { setTimeout(() => { resolve('Data received'); }, 1000); }); promise.then(result => { console.log(result); // Data received });

What happens here:

  • new Promise() creates the promise.
  • The callback receives two arguments: resolve() marks a successful completion, reject() marks an error.
  • .then() runs on a successful result.
  • .catch() catches errors.

Before ES6: callback hell

Before 2015 the main way to work with asynchronous code looked roughly like this:

javascript
getData(function (res) { process(res, function (result) { save(result, function (response) { console.log('Done!', response); }); }); });

Reading and debugging such code is painful: errors have to be caught by hand at every level, and the logic drowns in nesting. That is exactly what is known as callback hell.

Promise: the foundation ES6 gave us

A promise describes the "promise" to return a result once the operation finishes, and it has three states: pending, fulfilled and rejected. It leaves pending exactly once and never goes back.

Error handling becomes predictable:

javascript
const promise = new Promise((resolve, reject) => { const success = false; if (success) resolve('OK'); else reject('Error'); }); promise .then(res => console.log('Success:', res)) .catch(err => console.log('Error:', err));

Promise chaining

Several .then() calls can be linked together: the result of one step is passed to the next.

javascript
fetch('/api/data') .then(res => res.json()) .then(data => process(data)) .then(result => save(result)) .catch(error => console.error(error));

The code became linear and readable, unlike nested callbacks. A single .catch() at the end intercepts an error from any step of the chain.

Static Promise methods

MethodWhat it does
Promise.resolve(value)Creates an already fulfilled promise
Promise.reject(error)Creates a rejected promise
Promise.all([p1, p2, ...])Runs the promises in parallel and waits for all of them
Promise.race([p1, p2, ...])Returns the result of the first settled promise

A Promise.all example:

javascript
Promise.all([ fetch('/user'), fetch('/posts'), fetch('/comments') ]) .then(([userRes, postsRes, commentsRes]) => { console.log('All data received!'); }) .catch(err => console.error('Error:', err));

If even one promise is rejected, the whole Promise.all() fails with that error.

What came later

The question is about ES6 specifically, but it helps to see the whole evolution:

VersionNew for asynchrony
ES6 (2015)Promise, .then(), .catch(), Promise.all(), Promise.race()
ES2017async/await, syntactic sugar over promises
ES2020 and laterPromise.allSettled(), Promise.any(), Promise.withResolvers()

So ES6 became the foundation, and the later versions only improved and simplified working with it.

Why it matters:

Before ES6After ES6
Callbacks and callback hellPromises and a readable structure
Errors caught by hand at every level.catch() for centralised handling
Deep nestingChains of .then()
No guarantees about execution orderControllable, predictable asynchrony

In short: before ES6 asynchrony rested on callbacks, ES6 gave promises as a controllable model, and ES2017 added async/await on top of them.

Common mistakes

  1. Not returning the promise inside .then(). Without return the next step receives undefined and does not wait for the operation:

    javascript
    fetch('/api/data') .then(res => { res.json(); }) // no return .then(data => console.log(data)); // undefined
  2. Wrapping something that is already a promise. new Promise(resolve => fetch(url).then(resolve)) is a redundant wrapper that also swallows errors; returning fetch(url) itself is enough.

  3. Forgetting .catch(). An unhandled rejection produces an unhandled promise rejection, and in Node.js it can terminate the process.

  4. Believing that Promise.all starts the operations. Promises start the moment they are created, so Promise.all only waits for them; creating them one by one in a loop with await runs them sequentially instead of in parallel.

  5. Reaching for Promise.all where Promise.allSettled is needed. The former fails on the first rejection and loses the successful results; when you need the fate of every operation, use allSettled.

  6. Confusing a promise with a synchronous value. .then() always runs asynchronously, in the microtask queue, so the code right after the call executes before the handler.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.