for sync loop

Problem

When I had initially seen the new syntax for for await, my initial thought was it would behave similar to Promise.all and handle whatever was in the for loop concurrently. This is not what it does, BUT I am happy it solves asynchronous streaming.

None the less, it still did not solve this reoccurring pattern I find when developing. The need to perform many async tasks concurrently, inconveniently requires accumulating an array of promises to then be called by Promise.all.

Example Problems

Taking this as an example, you can see the requests array is now completely useless after resolving the promises, when being resolved after iteration would be more beneficial.

const requests = [];

for (const id of ids) {
  requests.push(fetch(id));
}

const responses = await Promise.all(requests);

As another example, with the fetch API, there are likely more async operations to await other than the response, like parsing based on content type, this is different, but caused by the same problem of having to accumulate these promises

const requests = [];

for (const id of ids) {
  requests.push(fetch(id).then(response => response.json()));
  // OR
  requests.push(new Promise(async (resolve) => {
    const response = await fetch(id);
    resolve(await response.json());
  });
}

const users = await Promise.all(requests);

And in some cases, you may even want users to be a Map of id to user, which adds another layer of complexity

const users = new Map();

for (const id of ids) {
  users.set(id, new Promise(async (resolve) => {
    const response = await fetch(id);
    resolve(await response.json());
  });
}

await Promise.all(users.values());
// Now you can use users map 😖

Solution

This is where for sync comes in, this would provide the ability to perform asynchronous operations iteratively and concurrently. Showing below as an example

const users = new Map();

for sync (const id of ids) {
  const response = await fetch(id);
  users.set(id, await response.json());
}

for sync sounds like a short-hand syntax for the Promise.all() + array.map() pattern, which could be nice. If we were to use that pattern, many of these examples would become simpler.

Example 1:

// Now there's no unnecessary requests array.
await Promise.all(ids.map(async id => {
  const response = await fetch(id);
  // ...use response however you want...
}));

Example 2:

const users = await Promise.all(ids.map(async id => {
  return fetch(id).then(response => response.json());
  // or
  const response = await fetch(id);
  return await response.json();
}));

Example 3:

const users = new Map();

await Promise.all(ids.map(async id => {
  const response = await fetch(id)
  const user = await response.json();

  users.set(id, user);
}))

// Now you can use users map 🙂

// alternative solution that I would prefer:

const userEntries = await Promise.all(ids.map(async id => {
  const response = await fetch(id)
  const user = await response.json();

  return [id, user];
}));

const users = new Map(userEntries);

The usage of the Promise.all()+array.map() pattern makes the stated issues go away, the same as the proposed for sync construct. Even still, I do fine the combination of those two functions to be cumbersome to use - await Promise.all(ids.map(async id => is a bit of a mouthful.

But, one thing that this pattern has going for it, is that it returns a value, something that a for sync loop can't do (see my second version of example 3, where instead of building a map via mutation, I was able to construct one using entries up-front). It would be nice if any new syntax (or, perhaps new function) were to keep this sort of property.

There are definitely acceptable alternatives to the examples I made, I think with or without map though, the solutions are still quite abstruse and could use much simpler syntax.

The root cause of the dilemma, I am seeing is this pattern of having to accumulate promises in order to handle them concurrently.

Many programming languages already have syntax for easier concurrency, and often times it's raved about by their respective communities. Like Go's go routines and R's %dopar% operator.

await.all cleans up all+map a little

await.all ids.map(async id => { ...

While I think the approach of for sync is interesting, I think it also comes off a bit confusing. It appears to try to be both sync and async at the same time...? Is it more like an awaited all+map or running an async do block in an otherwise normal loop?

for (const id of ids) {
  async do {
    const response = await fetch(id);
    users.set(id, await response.json());
  }
}

The sync keyword to me suggests the latter, but my understanding is that its intended to be more like the former. It also changes the behavior of await so that it is no longer blocking in its function which could cause confusion as well.

P.S. You need to be careful when using async executors in Promise constructors. Errors thrown in async functions are not handled by the constructor since they map to rejecting the returned promise instead of being thrown from the call.

new Promise(()=>{
  throw "fail"
}
).catch(console.log) // "fail"

new Promise(async()=>{
  throw "fail"
}
).catch(console.log) // no log, Uncaught fail

Yeah, for most of my use cases I use resolve and reject in the constructor depending on if things fail, but I didn't add it in my examples above for easier readability.

await.all seems like it could be interesting, but if it could be used for a for loop, from my understanding it would behave like

// this
for await.all (const id of ids) {

}
// which is equivalent to this
for (const response of await.all responses) {

}

And again doesn't solve this problem of needing to accumulate all these promises. (and kind of defeats the common premise of using for await)

Not sure what the best syntax is for this, but I agree for sync seems a little confusing... Perhaps if we were to place await before for it could have the concurrent behavior? (open to suggestions)

const users = {};

await for (const id of ids) {
  const response = await fetch(id);
  users[id] = await response.json();
}