Chaining Multiple Promises

The last post chained two .then() calls to parse JSON. This one chains a whole second network request onto the same flat pattern.

July 11, 20264 min read8 / 22

The last post already had a small chain: fetch into .then(response => response.json()) into .then(data => ...). Stretching that same pattern to a real second request, fetching a neighboring country, turns out to need exactly one new idea.

What a .then() Actually Returns

Every .then() returns a new Promise, whether or not its callback returns anything. But if the callback does return a value, that value becomes the fulfilled value of the next .then() in the chain.

Promise.resolve(value) is a shortcut for a Promise that's already fulfilled with that value, useful right here for testing the chain without a real network call.

JavaScript · Live Editor
Loading editor...

Hit Run. The second .then() logs 23, not 'anything', because that's exactly what the first one returned. And if what gets returned is itself a Promise (a new fetch call, say), the chain waits for that Promise to settle before handing its value down to the next step.

Chaining a Real Second Request

That's the whole trick needed to fetch a neighboring country: return the second fetch call from inside the first one's .then(), instead of just calling it and moving on.

JavaScript · Live Editor
Loading editor...

Hit Run and both cards appear, Portugal then Spain, four .then() calls deep and still perfectly flat. That's the actual payoff: this chain could grow to ten steps, the neighbor of the neighbor of the neighbor, and every one of them would sit at this same indentation level.

Notice the last .then() takes data directly, no [data] destructuring this time. That's the same array-vs-object gotcha from a few posts back: searching by name returns an array, searching by an exact code returns the object itself.

The Guard Clause Has a Gap

That if (!neighborCode) return; line has a real problem, worth naming rather than hiding. An island with no borders hits that line and returns undefined instead of a Promise. The next .then() still expects a response to call .json() on, and undefined isn't one.

That's not a mistake to fix here. Handling it properly needs error handling, .catch() specifically, which is exactly what's next.

The Nesting Mistake This Chain Avoids

There's a very common way to get this wrong: chaining the second .then() directly inside the first one's callback instead of returning it.

JavaScript
// Don't: nesting recreates callback hell Promise.resolve('data').then((value) => { Promise.resolve(value.toUpperCase()).then((upper) => { console.log(upper); }); });
JavaScript
// Do: return the inner Promise and keep the chain flat Promise.resolve('data') .then((value) => Promise.resolve(value.toUpperCase())) .then((upper) => console.log(upper));

Both versions run and produce the same output, which is exactly why this mistake is easy to miss. The first one still works, it just quietly rebuilds the exact staircase Promises exist to escape, one callback nested inside another.

Chaining stays flat: each .then() sits at the same indentation level, while nesting one .then() inside another climbs back into the same staircase Promises are supposed to escape. ExpandChaining stays flat: each .then() sits at the same indentation level, while nesting one .then() inside another climbs back into the same staircase Promises are supposed to escape.

The Essentials

  1. A .then() callback's return value becomes the next .then()'s value.
  2. Returning a Promise from a .then() makes the chain wait for it before continuing.
  3. A chain can grow to as many steps as needed, ten or more, without ever nesting.
  4. Chaining a .then() inside another .then()'s callback recreates callback hell. Return the inner Promise instead.
  5. A guard clause that returns without a value passes undefined down the chain, breaking the next step. Fixed with error handling, next post.