Keeping Promise Chains Flat

When people are new to Promises, and habits of the callback-style asynchronous code are still fresh, it's common the code written to look the same stairway-shape way that it used to before switching to Promises. It's not uncommon to find code that looks something like the following:

const Promise     = require("bluebird");
const fakepromise = require("fakepromise");

function someAsyncFunction(userId) {

  return new Promise((resolve, reject) => {

    lookupUser(userId).then((user) => {
      let something = user.something;
      // some more calculations
      let newUserObj = transform(something, user);

      checkCondition(newUserObj).then((transformedUser) => {
        if (!transformedUser.passed) {
          reject(new Error("Didn't pass the condition!"));
        }

        saveToDatabase(transformedUser).then((result) => {
          if (verifyDbResult(result)) {
            resolve(result);
          } else {
            reject(new Error("Database malfunction"));
          }
        }).catch(err => {
          reject(err);
        });
      }).catch(err => {
        reject(err);
      });
    }).catch(err => {
      reject(err);
    });
  });
}

// Executing our test function:

someAsyncFunction("7fd12fd1-a977-4ace").then(result => {
  console.log("SUCCESS: ");
  console.log(result);
}).catch(err => {
  console.error(err.message);
  console.log("Please try again.");
});

//-- Faking-out support functions that are not essential:

function lookupUser(userId) {
  let user = {};
  user.id = userId;
  user.something = "somethingProp";
  return fakepromise.promise(1000, user);
}

function transform(something, user) {
  user.something = `was: ${something}`;
  user.converted = "9ac0758a-05fd-44c5-b6db-adccddd2d40b";
  return user;
}

function checkCondition(user) {
  let passed = getRandomFromRange(0, 3);
  // 2/3 chance of passing:
  passed = passed > 1 ? true : false;
  user.passed = passed;
  return fakepromise.promise(1000, user);
}

function saveToDatabase(user) {
  let errorRate = getRandomFromRange(0, 3);
  // 1/3 chance of erroring-out:
  errored = errorRate < 1 ? true : false;

  if (!errored) {
    user.source = 'from-database';
  }

  return fakepromise.promise(2000, user);
}

function verifyDbResult(user) {
  return user.source === "from-database";
}

function getRandomFromRange(min, max) {
  return Math.random() * (max - min) + min;
}

As you can notice, such code has the same hard-to-read shape as the callbacks-based code and isn't much nicer, even if it is correct. A clear telltale of our code having such problem is when you see one or more `.then()` calls inside a higher-level .then(), i.e. chaining of .then() calls. You should never need to start a new promise chain from an existing promise chain - doing so makes the logic of the code and the readability of it unnecessarily complicated. It can lead to errors as well. Due to these problems, Bluebird actively "fights" such style of writing promises and If you are using Bluebird promises, you will likely get a warning that states:

Warning: a promise was created in a handler but was not returned from it

The Flatter, Better Way

There's a relatively easy cure for all these headaches - adopt the style that is Promises-friendly: start returning each new promise from previous promises thus ending promises as early as possible. It als omeans that you can throw errors from subsequent promises, instead of rejecting - improving the readability of your code further. When written in the new style, the above code will like as follows:

const Promise     = require("bluebird");
const fakepromise = require("fakepromise");

function someAsyncFunction(userId) {

  return lookupUser(userId).then((user) => {
      const something = user.something;
      // some more calculations
      const newUserObj = transform(something, user);
      return checkCondition(newUserObj);
    })
    .then((transformedUser) => {
      if (!transformedUser.passed) {
        throw new Error("Didn't pass the condition!");
      }
      return saveToDatabase(transformedUser)
    })
    .then((userFromDB) => {
      if (verifyDbResult(userFromDB)) {
        return userFromDB;
      } else {
        throw new Error("Database malfunction");
      }
    });
}

Please note that we are only showing the changed function here. Full example should have all the other supporting functions, and the executing call, but they are exactly the same as in the previous example, earlier in this chapter. If you want the full source code, you can find it at: https://github.com/inadarei/promises123-code/blob/master/ex4.2.js

Last updated

Was this helpful?