DEV Community

Cover image for How to Display the Progress of Promises in JavaScript
Jr. Dev 👨🏾‍💻
Jr. Dev 👨🏾‍💻

Posted on

How to Display the Progress of Promises in JavaScript

Contents

  1. Overview
  2. Implementation
  3. Conclusion

Overview

Displaying the progress of multiple tasks as they are completed can be helpful to the user as it indicates how long they may need to wait for the remaining tasks to finish.

We can accomplish this by incrementing a counter after each promise has resolved.

The video version of this tutorial can be found here...

Our desired output will look something like this, as the tasks are in progress.

Loading 7 out of 100 tasks
Enter fullscreen mode Exit fullscreen mode

Implementation

Let's start with the markup!

All you need is a script tag to point to a JavaScript file (which will be implemented below), and one div element, whose text will be manipulated to update the progress counter of tasks completed.

<!DOCTYPE html>
<html>
<body>
    <div id="progress"></div>    
    <script src="app.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Next up, the JavaScript!

We will begin by creating a function which resolves a promise after a random time has passed.

We do this as it closely resembles how it will work in a real-world application, e.g. HTTP requests resolving at different times.

async function task() {
  return new Promise(res => {
    setTimeout(res, Math.random() * 5000);
  })
}
Enter fullscreen mode Exit fullscreen mode

Secondly, we will create an array of 100 promises and update the progress text to inform the user when all of the tasks have finished.

const loadingBar = document.getElementById('loadingBar');

(async() => {
  const promises = new Array(100)
    .fill(0)
    .map(task);

  loadingBar.textContent = `Loading...`;
  await Promise.all(promises);
  loadingBar.textContent = `Loading Finished`;
})();
Enter fullscreen mode Exit fullscreen mode

Now imagine if this takes 30 seconds to complete. All the user will see on screen is the text 'Loading...' whilst it is in progress.

That is not a very useful message!


Let's improve this now by updating the progress text after each task has resolved.

The code snippet below is the full implementation.

const loadingBar = document.getElementById('loadingBar');

async function task() {
  return new Promise(res => {
    setTimeout(res, Math.random() * 5000);
  })
}

function loadingBarStatus(current, max) {
  loadingBar.textContent = `Loading ${current} of ${max}`;
}

(async() => {
  let current = 1;
  const promises = new Array(100)
    .fill(0)
    .map(() => task().then(() => loadingBarStatus(current++, 100)));

  await Promise.all(promises);
  loadingBar.textContent = `Loading Finished`;
})();
Enter fullscreen mode Exit fullscreen mode

Now, you can see that as each promise is resolved, the counter is incremented and displayed to the user.


Conclusion

In short, all you need to do is update the progress as each promise is resolved.

I hope you found this short tutorial helpful.

Let me know your thoughts in the comments below. 😊

Discussion (7)

Collapse
lionelrowe profile image
lionel-rowe

This approach works fine if you don't care about any of the results of the promises, but not if you need to use those results (for example, if they're API responses):

const resolveToVal = val => new Promise(res => 
    setTimeout(() => res(val), Math.random() * 1000))

(async() => {
    const max = 5
    let count = 0

    const promises = [...new Array(max)]
        .map((_, i) => resolveToVal(i)
            .then(() => loadingBarStatus(++count, max)))

    const result = await Promise.all(promises)

    console.log(result) // [undefined, undefined, undefined, undefined, undefined]
})()
Enter fullscreen mode Exit fullscreen mode

Instead, you can store the promises to an intermediate variable, then loop over it with forEach for the side-effectey stuff, meanwhile awaiting it with Promise.all so you can do something with the result:

(async() => {
    const max = 5
    let count = 0

    const promises = [...new Array(max)]
        .map((_, i) => resolveToVal(i))

    promises.forEach(promise => promise
        .then(() => loadingBarStatus(++count, max)))

    const result = await Promise.all(promises)

    console.log(result) // [0, 1, 2, 3, 4]
})()
Enter fullscreen mode Exit fullscreen mode

You could further abstract the loading bar logic something like this:

const withLoadingBar = (promises, renderFn, doneMessage) => {
    let count = 0

    promises.forEach(promise => promise
        .then(() =>
            renderFn(`Loading ${++count} of ${promises.length}`)))

    const all = Promise.all(promises)

    all.then(() => renderFn(doneMessage))

    return all
}

(async() => {
    const max = 5

    const promises = [...new Array(max)]
        .map((_, i) => resolveToVal(i))

    const result = await withLoadingBar(promises, console.warn, 'Done!')

    console.log(result)
})()
Enter fullscreen mode Exit fullscreen mode
Collapse
shuckster profile image
Conan

Nice! I came up with something Promise-agnostic, although a little more cumbersome to use because of that:

function makeProgressNotifier(options) {
  const {
    min = 0,
    max = 100,
    initialValue,
    onUpdated = () => {},
  } = options || {}

  const range = max - min
  let current = initialValue

  return (getNextValue) => {
    const next = getNextValue(current)
    if (current === next) {
      return
    }
    current = next
    const fraction = (1 / range) * current
    onUpdated({ min, max, current, fraction })
  }
}
Enter fullscreen mode Exit fullscreen mode

Used something like this:

// setup
const update = makeProgressNotifier({
  min: 0,
  max: 100,
  initialValue: 0,
  onUpdated: ({ fraction }) => {
    const percentage = fraction * 100
    console.log('percentage = ', percentage)
  },
})

// progress
const promises = new Array(100).fill(0).map(() => {
  return task().then(() => {
    update((current) => current + 1) // <-- here
  })
})
Enter fullscreen mode Exit fullscreen mode

Thank you OP for the article and video, of course. 🙏

Collapse
zolotarev profile image
Andrew Zolotarev

Too complex.

Collapse
jbartusiak profile image
Jakub Bartusiak • Edited

I can't see an actual use case where this would be that useful. If you're waiting thirty seconds for something to complete and block the user, you're doing something wrong. Either way, this should be implemented on the backend with a progress endpoint or sth.

Collapse
jrdev_ profile image
Jr. Dev 👨🏾‍💻 Author

Hey, thanks for your comment.

A use case I used the other week was deleting multiple rows/data from a grid.

Another common example is app initialisation, how often do you open an app and there is a loading widget for a few seconds before you can use it?

Collapse
jbartusiak profile image
Jakub Bartusiak

I see what you mean, yet consider this.
When you'd delete data from the grid, I'm guessing that happens on the backend, right? Then, a better approach would be to remove the deleted items from the grid locally and notify the user whether the operation was successful or not.
For the second example, consider lazy loading some modules, or using placeholders for components being loaded (like skeletons).

Thread Thread
jrdev_ profile image
Jr. Dev 👨🏾‍💻 Author

I wouldn't say a better approach, but definitely a viable one dependent on the requirements 👍 Thanks for adding your comment though, definitely an alternative approach to consider