1

I have an array of promises that I would like to call in parallel, but resolve synchronously.

I made this bit of code to do the required task, however, I needed to create my own object QueryablePromise to wrap the native Promise that I can synchronously check to see it's resolved status.

Is there any better way to achieve this task that doesn't require a special object?

Please note. I do not want to use Promise.all as I don't want to have to wait for all promises to resolve before processing the effects of the promises. And I cannot use async functions in my code base.

const PROMISE = Symbol('PROMISE')
const tap = fn => x => (fn(x), x)

class QueryablePromise {
  resolved = false
  rejected = false
  fulfilled = false
  constructor(fn) {
    this[PROMISE] = new Promise(fn)
      .then(tap(() => {
        this.fulfilled = true
        this.resolved = true
      }))
      .catch(x => {
        this.fulfilled = true
        this.rejected = true
        throw x
      })
  }
  then(fn) {
    this[PROMISE].then(fn)
    return this
  }
  catch(fn) {
    this[PROMISE].catch(fn)
    return this
  }
  static resolve(x) {
    return new QueryablePromise((res) => res(x))
  }
  static reject(x) {
    return new QueryablePromise((_, rej) => rej(x))
  }
}

/**
 * parallelPromiseSynchronousResolve
 *
 * Call array of promises in parallel but resolve them in order
 *
 * @param  {Array<QueryablePromise>}  promises
 * @praram {Array<fn>|fn}  array of resolver function or single resolve function
 */
function parallelPromiseSynchronousResolve(promises, resolver) {
  let lastResolvedIndex = 0
  const resolvePromises = (promise, i) => {
    promise.then(tap(x => {
      // loop through all the promises starting at the lastResolvedIndex
      for (; lastResolvedIndex < promises.length; lastResolvedIndex++) {
        // if promise at the current index isn't resolved break the loop
        if (!promises[lastResolvedIndex].resolved) {
          break
        }
        // resolve the promise with the correct resolve function
        promises[lastResolvedIndex].then(
          Array.isArray(resolver)
            ? resolver[lastResolvedIndex]
            : resolver
        )
      }
    }))
  }
  
  promises.forEach(resolvePromises)
}

const timedPromise = (delay, label) => 
  new QueryablePromise(res => 
    setTimeout(() => {
      console.log(label)
      res(label)
    }, delay)
  )

parallelPromiseSynchronousResolve([
  timedPromise(20, 'called first promise'),
  timedPromise(60, 'called second promise'),
  timedPromise(40, 'called third promise'),
], [
  x => console.log('resolved first promise'),
  x => console.log('resolved second promise'),
  x => console.log('resolved third promise'),
])
<script src="https://codepen.io/synthet1c/pen/KyQQmL.js"></script>

Cheers for any help.

synthet1c
  • 6,152
  • 2
  • 24
  • 39
  • Consider creating an array of promises in which each promise is composed to include its effects and pass that array to `Promise.all`. – cartant Feb 10 '19 at 20:12
  • @cartant thanks for your comment, do you have an example of how that would work? – synthet1c Feb 10 '19 at 20:16
  • _"Is there any better way to achieve this task that doesn't require a special object?"_ What do you mean by _"better"_? Why do you believe a _"special object"_ is required? – guest271314 Feb 10 '19 at 20:20
  • I believe the special object is required as I need to check the status of the promises synchronously with my code above, You cannot do that with a native `Promise` hence I needed to wrap it with `QueryablePromise` which gives me the ability to check `promise.resolved` in the callback. I mean better by not having to wrap the promise, I am wondering if there is a functional js code convention or specific object that will get the task done, or if there is a tested library that I can use to perform the same task. – synthet1c Feb 10 '19 at 20:25
  • @synthet1c You can use `.then()`, see this [answer](https://stackoverflow.com/a/31424853/) at [Wait until all ES6 promises complete, even rejected promises](https://stackoverflow.com/q/31424561/); this pattern using jQuery can also be used without jQuery [Jquery Ajax prevent fail in a deferred sequential loop](https://stackoverflow.com/q/28131082/) – guest271314 Feb 10 '19 at 20:29
  • @guest271314 I have used a similar concept with `QueryablePromise` mine is just a little more verbose, it does seem like a good idea to reduce the code required, the only problem is you can't chain callbacks to native promise as you need to set the status before calling any further callbacks, it means you can't abstract away that part from the main code base. And you can't check the status from outside the promise internals. – synthet1c Feb 10 '19 at 20:36
  • Would suggest stating what you are trying to achieve and reducing the code to the bare essentials. If `async/await` was an option would point to this pattern, which may be even more verbose than the code at this users' [answer](https://stackoverflow.com/a/48349837/) to this question [Run multiple recursive Promises and break when requested](https://stackoverflow.com/questions/48349721/run-multiple-recursive-promises-and-break-when-requested), though might accomplish what you are trying to do. – guest271314 Feb 10 '19 at 20:44
  • @synthet1c See also [How do I handle multiple browser scripts making the same calls to the back-end service](https://stackoverflow.com/q/28107460/) – guest271314 Feb 10 '19 at 20:57
  • See the answers at [Promises for promises that are yet to be created without using the deferred \[anti\]pattern](https://stackoverflow.com/q/37426037/). You can pass a `callback` to a "queue" at this [answer](https://stackoverflow.com/a/37428749/), which is an updated version of the code at this users' answer at the previous link. – guest271314 Feb 10 '19 at 21:13
  • 1
    @guest271314 Actually I think you were correct, I was over complicating the code with `QueryablePromise` I just used an array internally to save the results, then check that array for the status of the promises rather than have the state on the promise itself. – synthet1c Feb 10 '19 at 21:27

2 Answers2

6

Using a for await...of loop, you can do this quite nicely if you already have the array of promises:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

(async () => {
  const promises = range(5, index => {
    const ms = Math.round(Math.random() * 5000);
    return delay(ms).then(() => ({ ms, index }));
  });

  const start = Date.now();

  for await (const { ms, index } of promises) {
    console.log(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
  }
})();

Since you can't use asynchronous functions, you can mimic the effect of for await...of by chaining the promises together using Array.prototype.reduce(), and synchronously scheduling a callback for each chain:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, cb) => array.reduce(
  (chain, promise, index) => chain.then(
    () => promise
  ).then(
    value => cb(value, index)
  ),
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return delay(ms).then(() => ms);
});

const start = Date.now();

asyncForEach(promises, (ms, index) => {
  console.log(`index ${index} resolved at ${ms}, consumed at ${Date.now() - start}`);
});

Error Handling

Since the promises were stated to be instantiated in parallel, I'll assume that errors on any individual promise will not propagate to other promises except through any potentially brittle chains constructed via asyncForEach() (like above).

But we also want to avoid cross-propagating errors between promises when chaining them together in asyncForEach(). Here's a way to schedule error callbacks robustly where errors can only propagate from the original promises:

const delay = ms => new Promise(resolve => { setTimeout(resolve, ms); });
const maybe = p => p.then(v => Math.random() < 0.5 ? Promise.reject(v) : v);
const range = (length, mapFn) => Array.from({ length }, (_, index) => mapFn(index));

const asyncForEach = (array, fulfilled, rejected = () => {}) => array.reduce(
  (chain, promise, index) => {
    promise.catch(() => {}); // catch early rejection until handled below by chain
    return chain.then(
      () => promise,
      () => promise // catch rejected chain and settle with promise at index
    ).then(
      value => fulfilled(value, index),
      error => rejected(error, index)
    );
  },
  Promise.resolve()
);

const promises = range(5, index => {
  const ms = Math.round(Math.random() * 5000);
  return maybe(delay(ms).then(() => ms)); // promises can fulfill or reject
});

const start = Date.now();

const settled = state => (ms, index) => {
  console.log(`index ${index} ${state}ed at ${ms}, consumed at ${Date.now() - start}`);
};

asyncForEach(
  promises,
  settled('fulfill'),
  settled('reject') // indexed callback for rejected state
);

The only caveat to note here is that any errors thrown in the callbacks passed to asyncForEach() will get swallowed by the error handling in the chain except for errors thrown within the callbacks on the last index of the array.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • Hi Patrick, yeah I noticed that too and added it into your asyncForEach function, and i added the ability to process each promise response with an individual callback or with a single callback and wrapped the entire function in `Promise.all` so I can continue the chain after all promises have resolved. Thank you for your help. – synthet1c Feb 11 '19 at 21:24
  • Interestingly, I'm unable to make this work in Typescript, it won't be able to predict settled('rejected') and the error with 2 arguments. – t.mikael.d Feb 14 '22 at 07:10
2

I would recommend to indeed use Promise.all - but not on all promises at once, rather all the promises that you want to have fulfilled for each step. You can create this "tree list" of promises with reduce:

function parallelPromisesSequentialReduce(promises, reducer, initial) {
  return promises.reduce((acc, promise, i) => {
    return Promise.all([acc, promise]).then(([prev, res]) => reducer(prev, res, i));
  }, Promise.resolve(initial));
}

const timedPromise = (delay, label) => new Promise(resolve =>
  setTimeout(() => {
    console.log('fulfilled ' + label + ' promise');
    resolve(label);
  }, delay)
);

parallelPromisesSequentialReduce([
  timedPromise(20, 'first'),
  timedPromise(60, 'second'),
  timedPromise(40, 'third'),
], (acc, res) => {
  console.log('combining ' + res + ' promise with previous result (' + acc + ')');
  acc.push(res);
  return acc;
}, []).then(res => {
  console.log('final result', res);
}, console.error);
synthet1c
  • 6,152
  • 2
  • 24
  • 39
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Interesting approach! Mind if I edit my `asyncForEach()` implementations to use `reduce()` without `map()` like you have here? – Patrick Roberts Feb 11 '19 at 23:32
  • I went ahead and made the changes I was asking to. If you feel that my edit is not different enough from your answer, please feel free to revert it. – Patrick Roberts Feb 11 '19 at 23:52
  • @PatrickRoberts I'm glad that you like it :-) Actually I feel that your update is still not similar enough: `chain.then(() => promise)` causes an unhandled rejection warning if `promise` rejects before `chain` fulfills - one really should use `Promise.all`. – Bergi Feb 12 '19 at 08:50
  • Look in the second part of my answer for proper error handling. Regardless of the order in which `chain` and `promise` settle, the rejection is still handled by the second argument of the following `then()` call. – Patrick Roberts Feb 12 '19 at 08:55
  • Ah, now I see what you're referring to. The rejection warning is just that: a warning. Since the code does eventually handle the rejection, nothing is wrong. It's just that the browser is not sophisticated enough to actually emit the warning when the promise is garbage collected without its rejection being handled, rather it emits prematurely and then is handled after. – Patrick Roberts Feb 12 '19 at 09:07
  • In addition, another point is that `chain.then(() => promise)` is _intentionally_ different behavior than `Promise.all([chain, promise])`. It is guaranteed to settle only after `chain` has settled, even if `promise` rejects earlier, whereas `Promise.all()` will settle as soon as `promise` rejects, even if `chain` hasn't settled. – Patrick Roberts Feb 12 '19 at 09:16
  • 1
    @PatrickRoberts OK, I wasn't sure what the intention was, but if you want to handle the exception later (in array order), then you should suppress the potential warning with an explicit `promise.catch(e => {/* ignore til later*/});`. – Bergi Feb 12 '19 at 11:57
  • Thanks, I did that as well. – Patrick Roberts Feb 12 '19 at 16:13