DEV Community

Nik
Nik

Posted on

101 series: Promises #2: how to get current promise status and build your own promise queue?

1. How promises work

The first article described the base information about promises.
Before we get to more complicated topics, I'd suggest thinking about the tools and utility functions we can create on top of promises. For this article let's think about getting the current promise status and building a promise queue.

TL&DR

Check promise status
const pending = {
  state: 'pending',
};

function getPromiseState(promise) {
  // We put `pending` promise after the promise to test, 
  // which forces .race to test `promise` first
  return Promise.race([promise, pending]).then(
    (value) => {
      if (value === pending) {
        return value;
      }
      return {
        state: 'resolved',
        value
      };
    },
    (reason) => ({ state: 'rejected', reason })
  );
}
Enter fullscreen mode Exit fullscreen mode

Promise queue
class Queue {
  _queue = Promise.resolve();

  enqueue(fn) {
    const result = this._queue.then(fn);
    this._queue = result.then(() => {}, () => {});

    // we changed return result to return result.then() 
    // to re-arm the promise
    return result.then();
  }

  wait() {
    return this._queue;
  }
}
Enter fullscreen mode Exit fullscreen mode

How to get current promise status

After you created a promise, you cannot get an info at runtime whether the promise is still pending, fulfilled or rejected.

You can see the current promise status in debugger:
DevTools shows the promise status

From the debugger we see that the Symbol [[PromiseState]] is in charge of that. However, PromiseState is not well-known symbol, so we can treat this state, as a private field which we can't get "right now".

However, we can use Promise.race to test the current promise status. To do so, we may use the feature of Promise.race:

📝 Promise.race checks promises in their order. For example:

const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([a, b]).then(console.log); // 1
Enter fullscreen mode Exit fullscreen mode

And at the time:

const a = Promise.resolve(1);
const b = Promise.resolve(2);
Promise.race([b, a]).then(console.log); // 2
Enter fullscreen mode Exit fullscreen mode

📝 This code tests the promise status:

const pending = {
  state: 'pending',
};

function getPromiseState(promise) {
  // We put `pending` promise after the promise to test, 
  // which forces .race to test `promise` first
  return Promise.race([promise, pending]).then(
    (value) => {
      if (value === pending) {
        return value;
      }
      return {
        state: 'resolved',
        value
      };
    },
    (reason) => ({ state: 'rejected', reason })
  );
}
Enter fullscreen mode Exit fullscreen mode

Usage https://codesandbox.io/s/restless-sun-njun1?file=/src/index.js

(async function () {
  let result = await getPromiseState(Promise.resolve("resolved hello world"));
  console.log(result);

  result = await getPromiseState(Promise.reject("rejected hello world"));
  console.log(result);

  result = await getPromiseState(new Promise(() => {}));
  console.log(result);

  result = await getPromiseState("Hello world");
  console.log(result);
})();
Enter fullscreen mode Exit fullscreen mode

In addition to the check, this feature of Promise.race can be helpful to run some code with timeouts. E.g:

const TIMEOUT = 5000;
const timeout = new Promise((_, reject) => setTimeout(() => reject('timeout'), TIMEOUT));

// We may want to test check if timeout is rejected 
// before time consuming operations in the async code
// to cancel execution
async function someAsyncCode() {/*...*/}

const result = Promise.race([someAsyncCode(), timeout]);
Enter fullscreen mode Exit fullscreen mode

How to build your own promise queue

Sometimes you have to execute different code blocks in some order one-after-another. It's useful, when you have many heavy async blocks, which you want execute sequentially. We should remember, that:
📝 JS is single-threaded and therefore, we can have concurrent execution, but not parallel.

To do so, we can implement promise queue. This queue would put every function after all previously added async functions.

Let's check this codeblock:

class Queue {
  // By default the queue is empty
  _queue = Promise.resolve();

  enqueue(fn) {
    // Plan new operation in the queue
    const result = this._queue.then(fn);

    // avoid side effects. 
    // We can also provide an error handler as an improvement
    this._queue = result.then(() => {}, () => {});

    // To preserve promise approach, let's return the `fn` result
    return result;
  }

  // If we want just to understand when the queue is over
  wait() {
    return this._queue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Let's test it:

// Work emulator
const emulateWork = (name, time) => () => {
    console.log(`Start ${name}`);
    return new Promise((resolve) => {setTimeout(() => console.log(`End ${name}`) || resolve(), time)})
}

// Let's check if the queue works correctly if the promise fails
const failTest = () => () => {
    console.log(`Start fail`);
    return Promise.reject();
}
const queue = new Queue();
queue.enqueue(emulateWork('A', 500));
queue.enqueue(emulateWork('B', 500));
queue.enqueue(emulateWork('C', 900));
queue.enqueue(emulateWork('D', 1200));
queue.enqueue(emulateWork('E', 200));
queue.enqueue(failTest());
queue.enqueue(emulateWork('F', 900));
Enter fullscreen mode Exit fullscreen mode

We would have the output:
Promise Queue output

However, when the promise gets rejected, we swallowed the error completely.
It means, if the real code fails, we may never know about it.

📝 If you work with promises, remember to use .catch at the end of the promise chain, otherwise you may miss failures in your application!

To fix the situation we can either provide a callback for the constructor:

class Queue {
  _queue = Promise.resolve();

  // By default onError is empty
  _onError = () => {};

  constructor(onError) {
    this._onError = onError;
  }

  enqueue(fn) {
    const result = this._queue.then(fn);

    this._queue = result.then(() => {}, this._onError);

    return result;
  }

  wait() {
    return this._queue;
  }
}
Enter fullscreen mode Exit fullscreen mode

Or we can re-arm the promise!

class Queue {
  _queue = Promise.resolve();

  enqueue(fn) {
    const result = this._queue.then(fn);
    this._queue = result.then(() => {}, () => {});

    // we changed return result to return result.then() 
    // to re-arm the promise
    return result.then();
  }

  wait() {
    return this._queue;
  }
}
Enter fullscreen mode Exit fullscreen mode

The same test would report a error!
Error handling

📝 Every promise chain may cause an unhandledRejection error.

If you want to experiment with the code we build: https://codesandbox.io/s/adoring-platform-cfygr5?file=/src/index.js

Wrapping up

Promises are extremely helpful when we need to resolve some complex async interactions.

In the next articles we will talk why JS promises are called sometimes as thenable objects and continue our experiments.

Discussion (0)