DEV Community

Quentin Ménoret for Doctolib Engineering

Posted on • Updated on

Using Promises as a queue

Promises in JavaScript are a very powerful tool, and there are a lot of applications that we never think of in the first place. One of them is to use Promises as a queue.

Let’s imagine you want to execute a number of asynchronous operations in order, one after another. If you know the operations in advance, your code will look like this:

await operation1()
await operation2()
await operation3()
Enter fullscreen mode Exit fullscreen mode

But what if you have an arbitrary number of operations to run? And if each of them can trigger more? Then you need a queuing system.

If we come back to the previous case, we could also have written it this way:

operation1()
  .then(operation2)
  .then(operation3)
Enter fullscreen mode Exit fullscreen mode

Which translates in pseudo code to

Promise
  .then(FunctionReturningPromise)
  .then(FunctionReturningPromise)
Enter fullscreen mode Exit fullscreen mode

Then, queuing an operation would be as easy as this:

add(operation) {
  queue.then(operation)
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately, with this implementation, you will always execute your .then on the same promise. We need to keep track of what is the last element:

add(operation) {
  queue = queue.then(operation)
}
Enter fullscreen mode Exit fullscreen mode

The code is still very wrong, because if one operation throws, the queue will stop. For instance, in this code, ‘second message’ will never appear on your screen:

Promise
  .resolve()
  .then(() => console.log(first message))
  .then(() => { throw new Error(an error) })
  .then(() => console.log(second message))
Enter fullscreen mode Exit fullscreen mode

One way to avoid this is to add a catch statement after every .then:

add(operation) {
  queue = queue.then(operation).catch(() => {
    // do whatever, log the error?
  })
}
Enter fullscreen mode Exit fullscreen mode

Now it’s better; we still need to initialise the queue though. A very easy way to do this is to actually generate a Promise that is already resolved:

queue = Promise.resolve()
Enter fullscreen mode Exit fullscreen mode

This gives us a complete implementation here:

class PromiseQueue {
  queue = Promise.resolve()

  add(operation) {
    this.queue = this.queue.then(operation).catch(() => {})
  }
}
Enter fullscreen mode Exit fullscreen mode

Drawback: with this simple implementation, you don’t get feedback on whether or not your operation succeeded in the code where you add the operation to the queue. You could also implement add so that it returns a promise that resolves once this specific operation has resolved (so you can get the feedback). This should do the trick:

class PromiseQueue {
  queue = Promise.resolve(true)

  add(operation) {
    return new Promise((resolve, reject) => {
      this.queue = this.queue
        .then(operation)
        .then(resolve)
        .catch(reject)
    })
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (3)

Collapse
 
congan profile image
王从安

Nice!I spent a few minutes registering for the DEV community to like.

Collapse
 
ws333 profile image
Bjørnar Hvidsten

Nice solution! Thanks for sharing 💚

Collapse
 
lukemt profile image
lukemt

I love it!