2

I am trying to control the flow of the execution in my code below, meaning I want it to be serial.

I am reading and updating data from and to my DB, and ofc I want that to happen in the correct order. Below is the function I am calling my DB from, the queries functions are wrapped in callbacks.

I am pretty new to promises so perhaps the error might be something silly I am overlooking. If you need anything to ask please do so.

function my_function(array, array2)
{
    var array3 = [];

    return Promise.resolve(true)
    .then(function()
    {
        console.log("1")
        for(var i=0; i< array.length; i++)
        {
            get(array[i], function(results){
                console.log("2")
                array3.push(..);
            });
        }
        return array3;
    }).then(function()
    {   
        console.log("3")
        for(var i=0; i< array2.length; i+=2)
        {
            //...
            get(array2[i], function(results){
                console.log("4")
                return array3.push(...);
            });
        }
        return array3;
    }).then(function(array3)
    {   
        console.log("5")
        for(var i=0; i<array3.length; i++)
        {
            get(array3[i], function(results){
                console.log("6")
                update(.., function(callb_result){
                    return;
                });
            });
        }
    });
}

And here is the way I am calling the queries.

function get(array, callback)
{
    db.get(`SELECT .. FROM .. WHERE ..;`, function(error, row) {
        ...
        return callback(something);
    });
}

function update(.., callback)
{ 
   db.run(`UPDATE .. SET ...`);
   return callback("updated"); //I dont want to return anything
}

Whats printed in the log

1
3
5
2
4
6

I was thinking perhaps the way I ma calling the queries is async and that's messing up everything.

We're All Mad Here
  • 1,544
  • 3
  • 18
  • 46
  • `perhaps the way I ma calling the queries is async and that's messing up everything.` - yes, that's exactly the problem - your code makes it look like you think that promises somehow make asynchronous code synchronous. – Jaromanda X Mar 27 '17 at 02:29
  • 1. The `for..` loops don't wait for the `get` to finish. 2. `then` does not convert callback based code to promises (they ONLY accept promises to work properly, all other kinds of code are assumed to be synchronous and the next .then will not wait for the previous .then to finish). So you can't just do a single `Promise.resolve` you need to do a `new Promise...` inside each then to convert your callbacks to promises. 3. You need to Promise.all to process an array of promises – slebetman Mar 27 '17 at 02:53
  • oh great okay, I understood what you said about 2,3, regrading the 1st point you made, how should I tackle that? @slebetman – We're All Mad Here Mar 27 '17 at 03:13
  • For callback based code look at this: http://stackoverflow.com/questions/4631774/coordinating-parallel-execution-in-node-js/4631909?s=1|4.5544#4631909 . For promises construct an array of promises in your for loop then do this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all – slebetman Mar 27 '17 at 03:25
  • @slebetman I used promise.all, and added new promise's, as per 2,3 and indeed it _almost_ worked. I saw your old answer regrading the for loops, so the trick is to use the input variable and to resolve it when I am done doing whatever I am doing – We're All Mad Here Mar 27 '17 at 04:17
  • You won't get very far with this kind of thing until you [promisify the two low level functions](http://stackoverflow.com/documentation/javascript/231/promises/2109/promisifying-functions-with-callbacks#t=201703270742515542526). – Roamer-1888 Mar 27 '17 at 07:48

1 Answers1

1

You're using for loops to run asynchronous tasks and return an array that is modified by them. But because they are asynchronous the return happens before they are finished. Instead you can create an array of promises where each promise is one of the asynchronous tasks that resolves once the task is done. To wait until every task is done you can call Promise.all with the array of promises, which returns a promise that resolves with an array of the resolved results.

For the first .then you can use Array.prototype.map to easily create an array of promises. Each item in the array needs to return a new Promise that resolves with the result from the callback of get.

.then(function() {
  console.log("1");
  const promiseArray = array.map(function(item) {
    return new Promise(function(resolve) {
      get(item, function(result) {
        console.log("2");
        resolve(result);
      });
    });
  });
  return Promise.all(promiseArray);
})

As you return Promise.all the next .then call be executed once all the promises in the promiseArray are fulfilled. It will receive the array of results as the first parameter to the function. That means you can use them there. The second .then is similar to the first one, except that you don't want to call get on every item. In this case map is not applicable, so the for loop will just create a promise and add it to the array of promises. Before you have used array3 to store the results that you want to update, but with promises you don't really need that. In this case you can simply concat the results of both arrays.

.then(function(resultsArray) {
  console.log("3");
  const promiseArray2 = [];
  for (var i = 0; i < array2.length; i += 2) {
    const promise = new Promise(function(resolve) {
      get(array2[i], function(results) {
        console.log("4");
        resolve(results);
      });
    });
    promiseArray2.push(promise);
  }
  // Wait for all promises to be resolved
  // Then concatenate both arrays of results
  return Promise.all(promiseArray2).then(function(resultsArray2) {
    return resultsArray.concat(resultsArray2);
  });
})

This returns a promise that resolves with the concatenated array, so you will have all the results (from both .then calls) as an array, which is passed to the next .then function. In the third and final .then you simply call update on each element of the array. You don't need to call get again, as you've already done this and you passed on the results.

.then(function(finalResults) {
  console.log("5");
  for (var i = 0; i < finalResults.length; i++) {
    console.log("6");
    update(finalResults[i], function(result) {
      console.log(result);
    });
  }
});

Full runnable code (get uses a timeout to simulate asynchronous calls)

function myFunction(array, array2) {
  return Promise.resolve(true)
    .then(function() {
      console.log("1");
      const promiseArray = array.map(function(item) {
        return new Promise(function(resolve) {
          get(item, function(results) {
            console.log("2");
            resolve(results);
          });
        });
      });
      return Promise.all(promiseArray);
    })
    .then(function(resultsArray) {
      console.log("3");
      const promiseArray2 = [];
      for (var i = 0; i < array2.length; i += 2) {
        const promise = new Promise(function(resolve) {
          get(array2[i], function(results) {
            console.log("4");
            resolve(results);
          });
        });
        promiseArray2.push(promise);
      }
      return Promise.all(promiseArray2).then(function(resultsArray2) {
        return resultsArray.concat(resultsArray2);
      });
    })
    .then(function(finalResults) {
      console.log("5");
      for (var i = 0; i < finalResults.length; i++) {
        console.log("6");
        update(finalResults[i]);
      }
    });
}

function get(item, cb) {
  // Simply call the callback with the item after 1 second
  setTimeout(() => cb(item), 1000);
}

function update(item) {
  // Log what item is being updated
  console.log(`Updated ${item}`);
}

// Test data
const array = ["arr1item1", "arr1item2", "arr1item3"];
const array2 = ["arr2item1", "arr2item2", "arr2item3"];
myFunction(array, array2);

Improving the code

The code now works as expected, but there are many improvements that make it a lot easier to understand and conveniently also shorter.

To simplify the code you can change your get function to return a promise. This makes it a lot easier, since you don't need to create a promise in every step. And update doesn't need to be a promise, neither does it need a callback as it's synchronous.

function get(array) {
  return new Promise(function(resolve, reject) {
    db.get(`SELECT .. FROM .. WHERE ..;`, function(error, row) {
      if (err) {
        return reject(error);
      }
      resolve(something);
    });
  });
}

Now you can use get everywhere you used to create a new promise. Note: I added the reject case when there is an error, and you'll have to take care of them with a .catch on the promise.

There are still too many unnecessary .then calls. First of all Promise.resolve(true) is useless since you can just return the promise of the first .then call directly. All it did in your example was to automatically wrap the result of it in a promise.

You're also using two .then calls to create an array of the results. Not only that, but they perform exactly the same call, namely get. Currently you also wait until the first set has finished until you execute the second set, but they can be all executed at the same time. Instead you can create an array of all the get promises and then wait for all of them to finish.

function myFunction(array, array2) {
  // array.map(get) is equivalent to array.map(item => get(item))
  // which in turn is equivalent to:
  // array.map(function(item) {
  //   return get(item);
  // })
  const promiseArray = array.map(get);
  for (let i = 0; i < array2.length; i += 2) {
    promiseArray.push(get(array2[i]));
  }
  return Promise.all(promiseArray).then(results => results.forEach(update));
}

The myFunction body has been reduced from 32 lines of code (not counting the console.log("1") etc.) to 5.

Runnable Snippet

function myFunction(array, array2) {
  const promiseArray = array.map(get);
  for (let i = 0; i < array2.length; i += 2) {
    promiseArray.push(get(array2[i]));
  }
  return Promise.all(promiseArray).then(results => results.forEach(update));
}

function get(item) {
  console.log(`Starting get of ${item}`);
  return new Promise((resolve, reject) => {
    // Simply call the callback with the item after 1 second
    setTimeout(() => resolve(item), 1000);
  });
}

function update(item) {
  // Log what item is being updated
  console.log(`Updated ${item}`);
}

// Test data
const testArr1 = ["arr1item1", "arr1item2", "arr1item3"];
const testArr2 = ["arr2item1", "arr2item2", "arr2item3"];
myFunction(testArr1, testArr2).then(() => console.log("Updated all items"));
Michael Jungo
  • 31,583
  • 3
  • 91
  • 84