Mark Petherbridge

マーク・ペザーブリッジ

23 May 2019
Javascript Promise Example

This article is intended to be a short and sweet.

At work and for one of our projects we use Amazon Polly and I was assigned a JIRA ticket today to handle an error with the call to the API. At some point our application has grown bigger than originally expected and we are now getting the following error: ThrottlingException: Rate exceeded.

I feel that I must also point out that this is a great example of bad planning and as a developer you should always plan and build your applications with scalability in mind. "should" being the verb that strikes fear in developers.

Anyway, I thought great! This is just a quick fix that will be resolved using setTimeout()

After some initial digging, I found the function that made the call to AWS Polly:

function createAudioFiles(data, outputDir) {
    return new Promise((resolve, reject) => {
      let successessfullyCompletedAmount = 0;

      for ({ audioText, filename } of data) {

          createAudio(audioText, filename, outputDir)

          .then(({ status, message }) => {
            if (status == "success") {
              successessfullyCompletedAmount++;
            }

            // if all audio files have been created
            if (successessfullyCompletedAmount == data.length) {
              resolve({
                status: 'success',
                message: "successfully created audioFiles"
              })
            }

          })
      }
    });
  }

In the above function there is a for loop which contains a call to another function:

createAudio(audioText, filename, outputDir);

The only thing that is in that function is the AWS Polly SDK code so I will not list it all here. I wrapped that function call in a setTimeout() method with a 3 second timeout:

setTimeout(function() {
	createAudio(audioText, filename, outputDir);
}, 3000);

Job done, or so I thought. When I ran the code in a Terminal window I got the same Throttle Limit Exceeded message.

After some further investigation I realised by oversight and the issue was even though we were adding the timeout, it only delayed each one by 5 seconds and then added the next one straight behind it. So after 5 seconds, they all ran in sequence again.

[chained photo here]

What I needed to do was break up the block of data that is been sent and then to send those in batches. When and only when the previous block was completed. We can check when all the items of the data have been resolved by using the Promise.all() method.

The Promise.all() method returns a single Promise that resolves when all of the promises passed as an iterable have resolved or when the iterable contains no promises. It rejects with the reason of the first promise that rejects.

Refactoring the function

Inside my current promise return new Promise((resolve, reject) => { … } I created another function and added a function call:

let fnGet = (arr, size) => {
	// Functionality here.
}
fnGet(data, 30);

data (arr) is what is passed into the function wrapping this one and is an array of text to convert and output filenames. 30 (size) is is the amount of requests we want to send each time. At the moment, the data contains around 500 array objects and it is this what is causing the issues. I just put 30 as a random low number but you might want to check what the rate limits are for whatever service you are using this for.

Ok, now we want to “chop” up the array so that we have smaller sizes. For this I am going to use array slicing to take the required amount (30) off the array.

The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.
let remainder = [...(arr.slice(size))]

This line of code does two things: 1) It makes takes 30 objects from the arr array and reassigns arr with those 30 items and 2) whatever is left is assigned to the remainder variable.

I then created an empty array so I can add the generated promises:

promises = [];

What I now needed was to loop through the 30 (the size variable) items that are left in the array and send them to the API function and return a promise. I created a for loop which would iterate through the array, send the data to the function and store the returned promise into the promises array using the array.push method:

for(let i = 0 ; i < size ; i++) {
    promises.push(createAudio(arr[i].audioText, arr[i].filename, outputDir)
        .then(({ status, message }) => {
            if (status == "success") {
                successessfullyCompletedAmount++;
            }
        })
    )
}

The successessfullyCompletedAmount++ counter was already part of the code and I left that in just incase this is used somewhere else that I am not sure of yet.

Now I can implement the Promise.all method as mentioned above which check if all the promises resolve from the promises array. I can then chain functionality onto that method using the .then() method which will trigger when it returns true.

The then() method returns a Promise. It takes up to two arguments: callback functions for the success and failure cases of the Promise.

Below is the basic structure of the Promise.all().

Promise.all(promises)
.then( () => {
    // Do something.
}, (err) => {
    reject({
        status: 'failure',
        message: "An error occurred getting the audio"
    })
})

The section of code that will go into the then() part will be where we check if we have anything else to process or if we can resolve it.

Ok, so let’s check if there is anything else in the remainder to process and if not, then we can complete. We do this with a simple <span class="highlight"if condition statement:

if(remainder.length === 0) {
    resolve({
        status: 'success',
        message: "successfully created the audio Files"
    });
}

Now let’s turn that if statement into an if-else one because if there is something left to work on then we need to make sure we process it and add it to the promises array and remove it from the remainder array. This is also where we will be using the setTimeout() method that I mi-used earlier:

if(remainder.length === 0) {
    resolve({
        status: 'success',
        message: "successfully created audio Files"
    });
} else {
    setTimeout( () => {
        fnGet(remainder, size > remainder.length ? remainder.length : size);
    }, 1500)
}

And there you have it. A short and sweet technique to split apart a call to an API that returns a promise into smaller calls each with their own promises, we then wait for all those sub promises to be completed and then we can resolve the main promise returned.