3
\$\begingroup\$

I needed to execute a sequence of async tasks in JavaScript. They are async because I need them to be non-blocking, but I still want the current task to end before starting the next one. The order of tasks doesn't matter, the only important thing is that they are in mutual exclusion with each other.

For this reason, maybe because of C/C++ background, I was about to implement a queue and a system of locks. Then I stop to think if the problem could be solved just using async/await, and I ended up with this solution:

previousPromise = null;

async function enqueue(task) {
  while (previousPromise) {
    await previousPromise;
  }
  
  previousPromise = executeTask(task);
  await previousPromise;
  previousPromise = null;
}

In practice, I use the promise subscribers internal queue as the lock queue, in order to achieve a non-blocking wait. When the promise resolves, it awakes all the async tasks† and the first finding previousPromise equals to null will continue.

Here is a codepen example.

Is there any problem in this solution? Thank you

  • Side note 1: a real lock system is obviously not needed. JavaScript is single-threaded, so there isn't any risk to be preempted after the while and before executeTask assignment.

  • Side note 2: the tasks arrive from the server in an asynchronous fashion, so I can't collect them all before executing.

† It actually calls the first callback in the subscriber's list

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

This loop:

async function enqueue(task) {
  while (previousPromise) {
    await previousPromise;
  }

will (asynchronously) block forever, after the first time previousPromise has been assigned to. An expression which is a Promise (for example, the previousPromise) will always remain a Promise, and will be truthy. You can use await previousPromise to extract its resolve value into a different expression, but the previousPromise remains a Promise. No more than two tasks can be completed using that enqueue.

A better method would be to reassign the Promise every time enqueue is called, while chaining onto the previous Promise with a .then:

previousPromise = previousPromise.then(() => executeTask(task));

It'll also be easier to manage if the previousPromise stays the same shape (that is, of a Promise), rather than having to manage it possibly being null. Initialize it to Promise.resolve().

Demo below, where a task takes 1 second: it starts 2 tasks immediately, then 2 more tasks after 1.5 seconds (so these 4 together finish after 4 seconds), then another after 7 seconds (which finishes on second 8).

const executeTask = () => new Promise(resolve => setTimeout(() => {
  console.log('task resolving');
  resolve();
}, 1000));
let previousPromise = Promise.resolve();

function enqueue(task) {
  previousPromise = previousPromise.then(() => executeTask(task));
}

console.log('start');
enqueue();
enqueue();
setTimeout(() => {
  enqueue();
  enqueue();
}, 1500);
setTimeout(() => {
  enqueue();
}, 7000);

Another thing to consider is: what do you want to occur when one of the tasks rejects (if that's ever a possibility)? You might want to add on a .catch onto the end of executeTask to make sure the next task can be started, without the whole Promise chain breaking:

previousPromise = previousPromise
  .then(() => executeTask(task))
  .catch((err) => { /* log error? Make sure previousPromise always resolves */ });
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.