Pro Programmer
Callbacks And Promises

Callbacks and Promises

When you order something that needs some time to be prepared, they take your order and your name and tell you to wait to be called when your order is ready. After a while, they call your name and give you what you ordered.

The name you give them is like the callback function that you give to asynchronous APIs. It gets called with when the object that was requested is ready.

This is like when you order a latte from Starbucks (in the store, not in the drive-thru). They synchronously record your order and name and then you wait until your name is called. When that happens, you receive your latte:

starbucks.makeMeALatte(
  { type: 'Vanilla', size: 'Grande' },
  Samer
);

// "Samer" here is the callback function.
// When the Latte is ready, the barista will call Samer
// with the ready object
function Samer(readyLatte) {
  // drink readyLatte
}

Callbacks has many problems that can be partially solved by using promises instead. The difference between callbacks and promises in JavaScript is subtle but significant! Let’s try to understand it with a different analogy.

Let’s say you want to cook some rice and plain yogurt using a stove. Yes. You heard that right. You can cook plain yogurt and it’s extremely good when done right.

The problem is that cooking yogurt requires continuous stirring. If you stop stirring it burns. This means that while you’re stirring the yogurt you’re blocked from doing anything else (unless you turn the stove off and cancel the process).

Moreover, when the yogurt starts boiling the recipe at that point calls for lowering the heat, adding meat broth, and stirring some more.

If you’re the only one cooking you’ll need to do the yogurt stirring task synchronously! Your body, which is comparable to the single JavaScript thread in this analogy, is blocked for the duration of this synchronous task. You’ll have to finish the yogurt cooking before you can start on the rice. You can compare this to doing a loop in JavaScript:

putYogurtInPot()
putPotOnStove();

while (yogurtIsNotBoiling) {
  stirYogurt();
  // Now you're blocked until the loop is done
}

lowerHeat();
addMeatBroth(yogurt)

while (yogurtIsNotBoiling) {
  stirYogurt();
  // You're blocked again until the loop is done
}

turnStoveOff();
// Start on the rice

If you need to cook both the yogurt and rice simultaneously then you need to get some help. You need to delegate! You need another "thread". You need another person.

Your son is in the house and he happens to be free to help out. Great. You call him up and ask him to do the stirring for you.

Having someone else do the stirring here is like having an external module (like Node’s fs) do the slow IO work for you. Your son in this analogy is the Node module itself.

However, to work with an async resource (with Node’s fs methods for example) you need to use callbacks. The same goes for your son. You need to give him instructions (along with the raw yogurt and meat broth)! He might know how to stir but you need to tell him what to do with everything (and when to do it). You basically give him a callback of instructions and he is expected to execute these instructions at a certain point.

son.handleYogurtStirring(rawYogurt, (problem?, boilingYogurt) => {
  if (there is a problem) {
    reportIt();
  } else {
    lowerHeat();
    const yogurtMeatMix = addMeatBroth(boilingYogurt);
    handleYogurtStirring(yogurtMeatMix) // Another async operation
  }
});

// Start on the rice

By doing that, you free your single-threaded body to do something else. You can cook the rice now. You are using an asynchronous API.

So what is the problem? This is all good, isn’t it?

The problem with callbacks is that you lose control of what happens to the yogurt.

Not only is the stirring process itself now controlled by your helper, but the tasks that need to be done when the yogurt gets to a boiling point are also controlled by him. There is no guarantee that he will actually perform your instructions exactly like you described them.

  • Do you trust that he’ll correctly identify the boiling point?

  • Will he remember to put meat broth? Will he put enough and not over do it?

  • Will he remember to lower the heat?

You just do not have control and this is why we need promises! This is why I would make my son "promise" to follow instructions to the letter! If he does, the process becomes a promise-based one.

(async () => {
  try {
    const boilingYogurt = await son.handleYogurtStirring1(rawYogurt);
    son.lowerHeat();
    const yogurtMeatMix = son.addMeatBroth(boilingYogurt);
    const cookedYogurt = await son.handleYogurtStirring1(yogurtMeatMix);
  } catch(problem) {
    son.reportIt();
  }
})();

// Start on the rice

The only difference between handleYogurtStirring and handleYogurtStirring1 is that I was promised an outcome for handleYogurtStirring1. My helper verbally assured me he will remember to put meat broth and lower heat. I have a little bit of trust added to the equation

I used the async/await syntax to consume promises here but this is not really about async/await vs then/catch. However, you should favor the async/await syntax because it has a better flow that matches the way we analyze programs. Without async/await you’ll still need to use function nesting to accomplish some tasks. Some people call this promise hell!

Trust is great but we still do not have control. You can get some control by changing the nature of your instructions and having your son promise to notify you when the yogurt boils the first time and then you can add the meat broth to it yourself. This gives you better control but it also means that you need to be able to respond when notified, pause what you’re doing to handle the meat-broth task.

(async () => {
  try {
    const boilingYogurt = await son.handleYogurtStirring2(rawYogurt);
    you.lowerHeat();
    const yogurtMeatMix = you.addMeatBroth(boilingYogurt);
    const cookedYogurt = await son.handleYogurtStirring2(yogurtMeatMix);
  } catch(problem) {
    you: inspect(problem) && maybe(retry) || orderSomeTakeout();
  }
})();

// Start on the rice

The level of trust and control you get from promises depend on the library that you use. For example, let’s say you have a fancy electric cooker with a built-in stirring arm. You put raw yogurt in and you get cooked yogurt out. You can program the cooker to cook the yogurt for exactly 13.5 minutes (or whatever time is needed), and you can program it to sound an alarm if the built-in stirring arm is jammed. This cooker’s "API" is also a promise-based one because you have trust that it will either finish the process successfully or sound an alarm if something goes wrong. You have a lot of trust here!

(async() => {
  try {
    const cookedYogurt = await cooker.handleYogurtCooking(rawYogurt);
    // serve cookedYogurt
  } catch(problem) {
    you: inspect(problem) && maybe(retry) || orderSomeTakeout();
  }
})();

// Start on the rice

Not only that, but you also have a lot more control over this cooker. You can make sure it’s on a steady non-slip surface and that kids don’t mess with it. You can even plug it into some form of uninterruptible power supply. You have an actual promise object in this analogy. You can do things to it while it’s pending. The yogurt cooked with a cooker might not be as tasty as the one cooked on the stove but it’s certainly a more reliable outcome.

That’s really the difference between callbacks and promises. It’s not about syntax or nesting. It’s about control and trust.

More analogies? Let’s cook the rice!

One way to cook rice is to soak the dry rice in water for a while. You then get wet rice at which point you can put in pot on the stove and cover it with water and bring the water to the boiling point. You’ll then need to lower the heat, cover the lid, and steam the rice for a while.

All of these operations are asynchronous! While you’re waiting on the rice to soak, you can do something else, and so on. These operations also depend on each other. You can’t do them in parallel. You still need to do them in sequence. Asynchronous does NOT mean parallel.

Because you start with a dry-rice "object" and that same object becomes wet-rice (and then boiled-rice, and then steamed-rice), you can compare this process to working with promises as well.

// One way to consume these promises
dryRice.getSoacked()
  .then(wetRice =>
    wetRict.getBoiled()
      .then(boiledRice =>
        boiledRice.getSteamed()
          .then(steamedRice => serve(steamedRice));
      );
  );

// Or (Better)
dryRice.getSoacked()
  .then(wetRice => wetRict.getBoiled())
  .then(boiledRice => boiledRice.getSteamed())
  .then(steamedRice => serve(steamedRice));

// Or (Best)
(async () => {

  const wetRice = await dryRice.getSoacked();
  const boiledRice = await wetRict.getBoiled();
  const stearmedRice = await boiledRice.getSteamed();

  serve(steamedRice);

})();