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.allandPromise.race. async/awaitis ES2017, syntactic sugar on top of promises rather than a replacement for them.
Quick example
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:
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:
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.
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
| Method | What 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:
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:
| Version | New for asynchrony |
|---|---|
| ES6 (2015) | Promise, .then(), .catch(), Promise.all(), Promise.race() |
| ES2017 | async/await, syntactic sugar over promises |
| ES2020 and later | Promise.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 ES6 | After ES6 |
|---|---|
| Callbacks and callback hell | Promises and a readable structure |
| Errors caught by hand at every level | .catch() for centralised handling |
| Deep nesting | Chains of .then() |
| No guarantees about execution order | Controllable, 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
-
Not returning the promise inside
.then(). Withoutreturnthe next step receivesundefinedand does not wait for the operation:javascriptfetch('/api/data') .then(res => { res.json(); }) // no return .then(data => console.log(data)); // undefined -
Wrapping something that is already a promise.
new Promise(resolve => fetch(url).then(resolve))is a redundant wrapper that also swallows errors; returningfetch(url)itself is enough. -
Forgetting
.catch(). An unhandled rejection produces anunhandled promise rejection, and in Node.js it can terminate the process. -
Believing that
Promise.allstarts the operations. Promises start the moment they are created, soPromise.allonly waits for them; creating them one by one in a loop withawaitruns them sequentially instead of in parallel. -
Reaching for
Promise.allwherePromise.allSettledis needed. The former fails on the first rejection and loses the successful results; when you need the fate of every operation, useallSettled. -
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 readyA concise answer to help you respond confidently on this topic during an interview.