When Functions Are Nested Too Deep

Many JavaScript operations are asynchronous — they don't finish immediately. Loading a file, making a network request, or waiting for a timer all take time. Before Promises existed, the only way to handle " do this, then do that " was callbacks — functions passed as arguments to be called when the operation completes.

A Simple Callback

function loadScript(src, callback) {
  let script = document.createElement('script');
  script.src = src;
  script.onload = () => callback(null, script);
  script.onerror = () => callback(new Error('Load failed: ' + src));
  document.head.append(script);
}

loadScript('/script1.js', (err, script) => {
  if (err) { handleError(err); return; }
  console.log('Script 1 loaded!');
});

The Callback Hell Problem (Pyramid of Doom)

When you need to perform multiple sequential async operations, the code nests deeper and deeper:

loadScript('1.js', function() {
  loadScript('2.js', function() {
    loadScript('3.js', function() {
      loadScript('4.js', function() {
        // And so on... it keeps going right -->
      });
    });
  });
});

This code is extremely difficult to read, debug, and handle errors in. Each level of nesting requires its own error handling, making the code grow in both width and complexity.

Note

Promises were invented specifically to solve this problem, and async/await made it even more elegant. You'll learn both in the next lessons.