Before 2015, the main way to handle asynchronous tasks was with callbacks: you pass a function that should be called when the slow task finishes. Let's pretend we're loading some data from a server. (We'll use setTimeout to fake the slowness.)
function getUser(id, callback) {
setTimeout(() => {
callback({ id: id, name: "Olivia" });
}, 100);
}
getUser(1, (user) => {
console.log(`Loaded user: ${user.name}`);
});
console.log("Loading...");
It prints
Loading... Loaded user: Olivia
That works fine for one task. But real apps often need to do several slow things in order, where each step needs the result of the previous one. "Get the user, then get their orders, then get the items in the first order, then get the prices..." With callbacks, each step has to go inside the previous step's callback:
The code drifts further and further to the right, forming a sideways pyramid. Programmers call this callback hell or the Pyramid of Doom. It's hard to read, hard to change, and handling errors at every level is a nightmare.
There had to be a better way. And there is.