How to make run nested asynchronous methods synchronously?

253
December 03, 2017, at 7:09 PM

How do I wrap this routine inside a Promise so that I only resolve when I get all the data?

var accounts = [];
getAccounts(userId, accs => {
    accs.forEach(acc => {
        getAccountTx(acc.id, tx => {
            accounts.push({
                'id': acc.id,
                'tx': tx
            });
        });
    })
});

EDIT: Any issues if I do it like this?

function getAccountsAllAtOnce() {
    var accounts = [];
    var required = 0;
    var done = 0;
    getAccounts(userId, accs => {
        required = accs.length;
        accs.forEach(acc => {
            getAccountTx(acc.id, tx => {
                accounts.push({
                    'id': acc.id,
                    'tx': tx
                });
                done = done + 1;
            });
        })
    }); 
    while(done < required) {
        // wait
    }
    return accounts;
}
Answer 1

Let's put this routine into a separate function, so it is easier to re-use it later. This function should return a promise, which will be resolved with array of accounts (also I'll modify your code as small as possible):

function getAccountsWithTx(userId) {
  return new Promise((resolve, reject) => {
    var accounts = [];
    getAccounts(userId, accs => {
      accs.forEach(acc => {
        getAccountTx(acc.id, tx => {
          accounts.push({
            'id': acc.id,
            'tx': tx
          });
          // resolve after we fetched all accounts
          if (accs.length === accounts.length) {
            resolve(accounts);
          }
        });
      });
    });
  });
}

The single difference is just returning a promise and resolving after all accounts were fetched. However, callbacks tend your codebase to have this "callback hell" style, when you have a lot of nested callbacks, and it makes it hard to reason about it. You can workaround it using good discipline, but you can simplify it greatly switching to returning promises from all async functions. For example your func will look like the following:

function getAccountsWithTx(userId) {
  getAccounts(userId)
    .then(accs => {
       const transformTx = acc => getAccountTx(acc.id)
         .then(tx => ({ tx, id: acc.id }));
       return Promise.all(accs.map(transformTx));
    });
}

Both of them are absolutely equivalent, and there are plently of libraries to "promisify" your current callback-style functions (for example, bluebird or even native Node util.promisify). Also, with new async/await syntax it becomes even easier, because it allows to think in sync flow:

async function getAccountsWithTx(userId) {
  const accs = await getUserAccounts(userId);
  const transformTx = async (acc) => {
    const tx = getAccountTx(acc.id);
    return { tx, id: acc.id };
  };
  return Promise.all(accs.map(transformTx));
}

As you can see, we eliminate any nesting! It makes reasoning about code much easier, because you can read code as it will be actually executed. However, all these three options are equivalent, so it is up to you, what makes the most sense in your project and environment.

Answer 2

I'd split every step into its own function, and return a promise or promise array from each one. For example, getAccounts becomes:

function getAccountsAndReturnPromise(userId) {
    return new Promise((resolve, reject) => {
        getAccounts(userId, accounts => {
             return resolve(accounts);
        });
    });
};

And getAccountTx resolves to an array of { id, tx } objects:

function getAccountTransactionsAndReturnPromise(accountId) {
    return new Promise((resolve, reject) => {
        getAccountTx(account.id, (transactions) => {
             var accountWithTransactions = {
                 id: account.id,
                 transactions
             }; 
             return resolve(accountWithTransactions);
        });
    });
};

Then you can use Promise.all() and map() to resolve the last step to an array of values in the format you desire:

function getDataForUser(userId) {
  return getAccountsAndReturnPromise(userId)
  .then(accounts=>{
    var accountTransactionPromises = accounts.map(account => 
      getAccountTransactionsAndReturnPromise(account.id)
    );
    return Promise.all(accountTransactionPromises);
  })
  .then(allAccountsWithTransactions => {
    return allAccountsWithTransactions.map(account =>{ 
        return { 
            id: account.id, 
            tx: tx
        }
    });
  });
}
Rent Charter Buses Company
READ ALSO
error: web uses an image, skipping (docker compose)

error: web uses an image, skipping (docker compose)

I am currently trying out this tutorial for node express with mongodb https://mediumcom/@sunnykay/docker-development-workflow-node-express-mongo-4bb3b1f7eb1e

1123
Mongoose Save Callback is not working

Mongoose Save Callback is not working

I'm building a server for a simple web app that uses a MongoDB backend for storing dataI instantiate a new mongoose connection using:

338
Instead of running in two server ports(angular:4200 and node:3000) can we make it as one(angular+node:3000) Angular+Node+MongoDB

Instead of running in two server ports(angular:4200 and node:3000) can we make it as one(angular+node:3000) Angular+Node+MongoDB

How to deploy the localhost developed application with Angular+Node+MongoDB in productionPlease share the steps to follow

171
Mongoose Foregin Key mapping

Mongoose Foregin Key mapping

I'm new to MongoDB/Mongoose and trying to figure out how to map relationships between Schema

294