Callback Hell And Chained Ajax Requests

A few posts back, chaining two AJAX calls together already gave callback hell away by name. Time to actually build one and see exactly why it's a problem.

July 11, 20265 min read5 / 22

A few posts back, firing off two AJAX calls back to back meant they ran in parallel, in no particular order. That's fine when the two requests have nothing to do with each other. It falls apart the moment the second one actually needs data from the first.

Making the Second Call Depend on the First

Say the goal is: look up a country, then look up one of its neighbors right next to it. There's no way to know which neighbor to ask for until the first response actually arrives, the two requests have to run one after another, not at the same time.

The rendering function from the last post gets called twice now, once per card, so it picks up one new piece: an optional className, so the neighbor's card can end up styled a little differently once it exists. The full version shows up in the runnable example below.

Finding the Neighbor, Then Asking for It

The country data already has everything needed to find a neighbor: a borders array of country codes.

JavaScript
const data = { name: 'Portugal', borders: ['ESP'] }; const [neighborCode] = data.borders; if (!neighborCode) return; console.log(`${data.name}'s neighbor code is ${neighborCode}`);

Some countries, islands especially, have no neighbors at all (an empty borders array), so that guard clause matters, without it, an island would crash the whole function trying to look up a neighbor that doesn't exist. Assuming one does exist, that code becomes the second request's target, fired off from inside the first request's own load callback.

JavaScript · Live Editor
Loading editor...

Hit Run. Portugal's card renders first, then Spain's, slightly smaller, appears right after it, every single time. Reload as many times as you want, that order never changes, and it can't, there's no way for the second request to even exist before the first response tells it what to ask for.

The pyramid callback hell always makes: each dependent request nested one level deeper inside the previous one's callback, growing indentation to the right and down. ExpandThe pyramid callback hell always makes: each dependent request nested one level deeper inside the previous one's callback, growing indentation to the right and down.

A Gotcha Worth Knowing: Searching by Code Returns a Different Shape

The second request hits /countries/alpha/{code} instead of /countries/name/{country}, and the parsing changes to match: JSON.parse(this.responseText) directly, no array destructuring.

Here's why. A name search could match more than one country in theory, so it always wraps the answer in an array. A code search can only ever match one, codes are unique, so it hands back a bare object instead.

Same API, two different response shapes, depending on which endpoint answered. Check a response before assuming its shape.

One Callback Inside Another

Look at the indentation in that last code block. The second request's entire setup lives inside the first request's load callback, one function nested inside another.

That's fine, for two levels. The trouble starts when there's a third dependent request, then a fourth, each one only able to start once the last one's data arrives. Every additional level nests one callback deeper inside the one before it, and the indentation grows to match.

This isn't unique to AJAX, either. Any callback-based asynchronous code chains the same way. Four back-to-back timers make the exact same shape.

JavaScript · Live Editor
Loading editor...

Run it and watch the four lines print out a second apart, in order, while the code itself marches steadily rightward with every nested level. That growing, right-leaning staircase has an actual name among developers: callback hell.

Why This Is an Actual Problem, Not Just Ugly

Callback hell isn't a complaint about how the code looks. Deeply nested code is genuinely harder to read, harder to trace through, and harder to change safely, and code that's hard to reason about reliably ships more bugs than code that isn't.

That's a rule worth keeping generally, not just here: if code is hard to understand, it's bad code, no matter how correctly it happens to run today. The harder something is to follow, the harder it gets to add a fifth request, or fix a bug in the third one, without breaking the other four.

Nested callbacks are one very common way code ends up like that, but they're not the only way to write asynchronous JavaScript. Since ES6, there's a real way out: Promises, which is exactly what's next.

The Essentials

  1. A dependent request must fire from inside the previous request's own callback. That's the only way to guarantee order.
  2. Guard against missing data before firing a dependent request, an island has no borders to look up.
  3. The same API can return different response shapes per endpoint. Name search returns an array; code search returns a single object.
  4. Each dependent request nests one callback deeper than the last, forming a staircase. That shape is callback hell.
  5. Hard-to-read code is a real defect, not a style complaint. It ships more bugs and resists safe changes.