Promises chaining
Back in Introduction: callbacks we hit a wall: a run of asynchronous tasks that must happen in order — load one script, then the next, then the next — turned into a staircase of nested functions. Promises give you a cleaner way to write that same sequence.
The main tool is promise chaining. It reads top to bottom, each step feeding the next.
Here’s the shape of it:
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000); // (*)
}).then(function(result) { // (**)
alert(result); // 1
return result * 2;
}).then(function(result) { // (***)
alert(result); // 2
return result * 2;
}).then(function(result) {
alert(result); // 4
return result * 2;
});
The value travels down through the chain of .then handlers. Each handler receives what the last one returned, does something with it, and hands its own result to the next.
Walking the flow:
- The starting promise settles after one second at line
(*), with the value1. - That fires the first
.thenhandler(**). It alerts1, returns1 * 2. - The second
.then(***)receives2, alerts it, returns2 * 2. - The third receives
4, and so on.
So the alerts appear in order: 1 → 2 → 4.
resolves 1
gets 1 returns 2
gets 2 returns 4
gets 4
The reason this works: every .then call returns a fresh promise. That new promise is what you call the next .then on. When your handler returns a plain value, that value becomes the result of the returned promise, so the following handler receives it.
The alerts in that first example are hard to watch flowing. Here the same chain writes each step into the page instead, so you can see the return value of one handler arrive as the argument of the next. Try changing result * 2 to result + 10 and re-running.
Chaining is not the same as multiple handlers
Here’s a mistake that catches almost everyone once. You can attach several .then handlers to the same promise. That is not a chain.
let promise = new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
promise.then(function(result) {
alert(result); // 1
return result * 2;
});
Notice the difference from the first example: every .then is called on the same variable promise, not on the result of the previous .then. These handlers don’t feed each other. Each one independently receives the result of that single promise.
They all alert 1. The return result * 2 in each is ignored here, because nobody is listening to the promises those .then calls produce.
The demo below runs both versions on the same starting promise so you can compare the output directly. The chain builds up 1 → 2 → 4; the three independent handlers each see the untouched 1.
Attaching several handlers to one promise is rare in practice. Chaining is what you’ll reach for almost every time.
Returning promises
The value a handler returns doesn’t have to be a plain number or string. If a .then handler returns a promise, the chain pauses there: the next handler waits for that promise to settle, then receives its result.
new Promise(function(resolve, reject) {
setTimeout(() => resolve(1), 1000);
}).then(function(result) {
alert(result); // 1
return new Promise((resolve, reject) => { // (*)
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) { // (**)
alert(result); // 2
return new Promise((resolve, reject) => {
setTimeout(() => resolve(result * 2), 1000);
});
}).then(function(result) {
alert(result); // 4
});
The first .then alerts 1 and returns a brand-new promise at line (*). A second later that inner promise resolves with result * 2, and that value flows to the handler at (**), which alerts 2 and repeats the trick.
The alerts are still 1 → 2 → 4, but now there’s a one-second gap between each, because the chain genuinely waited for every inner promise.
This is the whole engine behind sequencing async work. Because a handler can return a promise, you can line up any number of asynchronous steps and they run strictly one after another.
Watch the timing yourself. Each handler below returns a promise that resolves after a short delay, and each line is stamped with how long the chain has been running. The gaps prove the chain actually waited for every inner promise before moving on.
Example: loadScript
Let’s put returned promises to work with the promisified loadScript from the previous chapter, loading three scripts in sequence:
loadScript("https://example.com/article/promise-chaining/one.js")
.then(function(script) {
return loadScript("https://example.com/article/promise-chaining/two.js");
})
.then(function(script) {
return loadScript("https://example.com/article/promise-chaining/three.js");
})
.then(function(script) {
// use functions declared in the scripts
// to prove they really loaded
one();
two();
three();
});
Arrow functions tighten it up:
loadScript("https://example.com/article/promise-chaining/one.js")
.then(script => loadScript("https://example.com/article/promise-chaining/two.js"))
.then(script => loadScript("https://example.com/article/promise-chaining/three.js"))
.then(script => {
// all three are loaded, their functions are available
one();
two();
three();
});
Each loadScript returns a promise. The next .then fires only when that promise resolves, and it kicks off the following load. The scripts arrive strictly in order.
The important thing about the shape: the code grows downward, not rightward. No staircase, no pyramid of doom.
You could nest .then calls instead, and it would still load three scripts in order:
loadScript("https://example.com/article/promise-chaining/one.js").then(script1 => {
loadScript("https://example.com/article/promise-chaining/two.js").then(script2 => {
loadScript("https://example.com/article/promise-chaining/three.js").then(script3 => {
// this innermost function can see script1, script2 and script3
one();
two();
three();
});
});
});
Same behavior, worse shape — it drifts to the right, and you’re back to the callback problem. Beginners who haven’t met chaining yet often write it this way. Prefer the flat chain.
There’s one honest reason to nest: the innermost callback can reach all the outer variables (script1, script2, script3) through closure. If you truly need every intermediate result together at the end, nesting gives you that for free. Treat it as the exception, not your default.
Bigger example: fetch
On the front end, promises show up constantly around network requests. Let’s build a fuller example.
The fetch method loads data from a server. It has many optional settings, but the core call is short:
let promise = fetch(url);
This fires a network request to url and returns a promise. That promise resolves with a response object as soon as the server sends back its headers — importantly, before the response body has finished downloading.
To read the full body as text, call response.text(). It returns another promise, which resolves once the whole text has arrived, with that text as its value.
fetch('https://example.com/article/promise-chaining/user.json')
// this .then runs when the server responds with headers
.then(function(response) {
// response.text() returns a promise that resolves with the full body text
return response.text();
})
.then(function(text) {
// ...and here is the downloaded content
alert(text); // {"name": "arjuncodes", "isAdmin": true}
});
The response object also offers response.json(), which reads the body and parses it as JSON in one move. That’s handier here. With arrow functions:
// same idea, but response.json() parses the body as JSON
fetch('https://example.com/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => alert(user.name)); // arjuncodes
Now let’s do something with that user. We’ll fire a second request to GitHub for their profile and show the avatar:
// request user.json
fetch('https://example.com/article/promise-chaining/user.json')
// parse it as json
.then(response => response.json())
// request the GitHub profile for that user
.then(user => fetch(`https://api.github.com/users/${user.name}`))
// parse the GitHub response as json
.then(response => response.json())
// show the avatar (githubUser.avatar_url) for 3 seconds
.then(githubUser => {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => img.remove(), 3000); // (*)
});
The code runs, but it hides a trap that snares a lot of promise newcomers. Look at line (*). How would you run something after the avatar has finished showing and been removed — say, open an edit form for that user? As written, you can’t. Nothing downstream knows when the setTimeout fires.
The fix: make that step return a promise that resolves once the avatar’s time is up.
fetch('https://example.com/article/promise-chaining/user.json')
.then(response => response.json())
.then(user => fetch(`https://api.github.com/users/${user.name}`))
.then(response => response.json())
.then(githubUser => new Promise(function(resolve, reject) { // (*)
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser); // (**)
}, 3000);
}))
// runs after the 3 seconds are up
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
Now the handler at (*) returns a new Promise that stays pending until resolve(githubUser) runs inside the timeout at (**). The final .then waits for exactly that moment, then fires.
Finally, pull the pieces into named, reusable functions. The chain reads almost like plain English:
function loadJson(url) {
return fetch(url)
.then(response => response.json());
}
function loadGithubUser(name) {
return loadJson(`https://api.github.com/users/${name}`);
}
function showAvatar(githubUser) {
return new Promise(function(resolve, reject) {
let img = document.createElement('img');
img.src = githubUser.avatar_url;
img.className = "promise-avatar-example";
document.body.append(img);
setTimeout(() => {
img.remove();
resolve(githubUser);
}, 3000);
});
}
// put them together:
loadJson('https://example.com/article/promise-chaining/user.json')
.then(user => loadGithubUser(user.name))
.then(showAvatar)
.then(githubUser => alert(`Finished showing ${githubUser.name}`));
// ...
Each function returns a promise, so the chain stays flat and every step is testable on its own.
Summary
When a handler in a chain — .then, .catch, or .finally, it makes no difference — returns a promise, the rest of the chain pauses until that promise settles. Once it does, its result (or its error) flows to the next handler.
That single rule — a returned promise makes the chain wait — is what turns .then from a one-shot callback into a way to sequence any amount of asynchronous work, kept flat and readable.