Asked  7 Months ago    Answers:  5   Viewed   33 times

Given the following code:

var arr = [1,2,3,4,5];

var results: number[] = await arr.map(async (item): Promise<number> => {
        await callAsynchronousOperation(item);
        return item + 1;
    });

which produces the following error:

TS2322: Type 'Promise<number>[]' is not assignable to type 'number[]'. Type 'Promise<number> is not assignable to type 'number'.

How can I fix it? How can I make async await and Array.map work together?

 Answers

74

The problem here is that you are trying to await an array of promises rather than a promise. This doesn't do what you expect.

When the object passed to await is not a Promise, await simply returns the value as-is immediately instead of trying to resolve it. So since you passed await an array (of Promise objects) here instead of a Promise, the value returned by await is simply that array, which is of type Promise<number>[].

What you need to do here is call Promise.all on the array returned by map in order to convert it to a single Promise before awaiting it.

According to the MDN docs for Promise.all:

The Promise.all(iterable) method returns a promise that resolves when all of the promises in the iterable argument have resolved, or rejects with the reason of the first passed promise that rejects.

So in your case:

var arr = [1, 2, 3, 4, 5];

var results: number[] = await Promise.all(arr.map(async (item): Promise<number> => {
    await callAsynchronousOperation(item);
    return item + 1;
}));

This will resolve the specific error you are encountering here.

Tuesday, June 1, 2021
 
Skipper
answered 7 Months ago
59

TL;DR

Don't use the pattern in the question where you get the promises, and then separately wait on them; instead, use Promise.all (at least for now):

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

While your solution does run the two operations in parallel, it doesn't handle rejection properly if both promises reject.

Details:

Your solution runs them in parallel, but always waits for the first to finish before waiting for the second. If you just want to start them, run them in parallel, and get both results, it's just fine. (No, it isn't, keep reading...) Note that if the first takes (say) five seconds to complete and the second fails in one second, your code will wait the full five seconds before then failing.

Sadly, there isn't currently await syntax to do a parallel wait, so you have the awkwardness you listed, or Promise.all. (There's been discussion of await.all or similar, though; maybe someday.)

The Promise.all version is:

const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);

...which is more concise, and also doesn't wait for the first operation to complete if the second fails quickly (e.g., in my five seconds / one second example above, the above will reject in one second rather than waiting five). Also note that with your original code, if the second promise rejects before the first promise resolves, you may well get a "unhandled rejection" error in the console (you do currently with Chrome v61; update: more recent versions have more interesting behavior), although that error is arguably spurious (because you do, eventually, handle the rejection, in that this code is clearly in an async function¹ and so that function will hook rejection and make its promise reject with it) (update: again, changed). But if both promises reject, you'll get a genuine unhandled rejection error because the flow of control never reaches const value2 = await p2; and thus the p2 rejection is never handled.

Unhandled rejections are a Bad Thing™ (so much so that soon, Node.js will abort the process on truly unhandled rejections, just like unhandled exceptions — because that's what they are), so best to avoid the "get the promise then await it" pattern in your question.

Here's an example of the difference in timing in the failure case (using 500ms and 100ms rather than 5 seconds and 1 second), and possibly also the arguably-spurious unhandled rejection error (open the real browser console to see it):

const getValue1Async = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 500, "value1");
  });
};
const getValue2Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, "error");
  });
};

// This waits the full 500ms before failing, because it waits
// on p1, then on p2
(async () => {
  try {
    console.time("separate");
    const p1 = getValue1Async();
    const p2 = getValue2Async();
    const value1 = await p1;
    const value2 = await p2;
  } catch (e) {
    console.error(e);
  }
  console.timeEnd("separate");
})();

// This fails after just 100ms, because it doesn't wait for p1
// to finish first, it rejects as soon as p2 rejects
setTimeout(async () => {
  try {
    console.time("Promise.all");
    const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);
  } catch (e) {
    console.timeEnd("Promise.all", e);
  }
}, 1000);
Open the real browser console to see the unhandled rejection error.

And here we reject both p1 and p2, resulting in a non-spurious unhandled rejection error on p2:

const getValue1Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 500, "error1");
  });
};
const getValue2Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, "error2");
  });
};

// This waits the full 500ms before failing, because it waits
// on p1, then on p2
(async () => {
  try {
    console.time("separate");
    const p1 = getValue1Async();
    const p2 = getValue2Async();
    const value1 = await p1;
    const value2 = await p2;
  } catch (e) {
    console.error(e);
  }
  console.timeEnd("separate");
})();

// This fails after just 100ms, because it doesn't wait for p1
// to finish first, it rejects as soon as p2 rejects
setTimeout(async () => {
  try {
    console.time("Promise.all");
    const [value1, value2] = await Promise.all([getValue1Async(), getValue2Async()]);
  } catch (e) {
    console.timeEnd("Promise.all", e);
  }
}, 1000);
Open the real browser console to see the unhandled rejection error.

In a comment you've asked:

Side question: will the following force waiting for both (and discarding the results) await p1 && await p2?

This has the same issues around promise rejection as your original code: It will wait until p1 resolves even if p2 rejects earlier; it may generate an arguably-spurious (update: or temporary) unhandled rejection error if p2 rejects before p1 resolves; and it generates a genuine unhandled rejection error if both p1 and p2 reject (because p2's rejection is never handled).

Here's the case where p1 resolves and p2 rejects:

const getValue1Async = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 500, false);
  });
};
const getValue2Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, "error");
  });
};

(async () => {
  try {
    const p1 = getValue1Async();
    const p2 = getValue2Async();
    console.log("waiting");
    await p1 && await p2;
  } catch (e) {
    console.error(e);
  }
  console.log("done waiting");
})();
Look in the real console (for the unhandled rejection error).

...and where both reject:

const getValue1Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 500, "error1");
  });
};
const getValue2Async = () => {
  return new Promise((resolve, reject) => {
    setTimeout(reject, 100, "error2");
  });
};

(async () => {
  try {
    const p1 = getValue1Async();
    const p2 = getValue2Async();
    console.log("waiting");
    await p1 && await p2;
  } catch (e) {
    console.error(e);
  }
  console.log("done waiting");
})();
Look in the real console (for the unhandled rejection error).

¹ "...this code is clearly in an async function..." That was true in 2017 when this question and answer were written. Since then, top-level await happened/is happening.

Tuesday, June 1, 2021
 
LukeP
answered 7 Months ago
55

You need to return something from your async function (a return inside a then does not return from the main function). Either a promise or something you await-ed.

Also, make sure to declare your go variable to avoid leaking it into the global space.

const go = async () => {

  const options = {
    uri: "http://www.somewebsite.com/something",
    transform: function(body) {
      return cheerio.load(body);
    }
  };

  return request(options)
    .then($ => {
      let scrapeTitleArray = [];
      $(".some-class-in-html").each(function(i, obj) {
        const data = $(this)
          .text()
          .trim();
        scrapeTitleArray.push(data);
      });
      return scrapeTitleArray;
    })
    .catch(err => {
      console.log(err);
    });
};

Since you are using an async function, you might want to take advantage of the await syntax also.

const go = async () => {

  const options = {
    uri: "http://www.somewebsite.com/something",
    transform: function(body) {
      return cheerio.load(body);
    }
  };

  try {
    const $ = await request(options);
    $(".some-class-in-html").each(function(i, obj) {
      const data = $(this)
        .text()
        .trim();
      scrapeTitleArray.push(data);
    });
    return scrapeTitleArray;
  }
  catch (err) {
    console.log(err);
  }
};
Thursday, August 12, 2021
 
Bitmap
answered 4 Months ago
20

When you call ConfigureAwait(false), the rest of the method will be executed on a thread pool thread unless the Task you're awaiting is already complete.

Since GetAsync will almost definitely run asynchronously, I would expect GetStringAsync to run on a thread pool thread.

public async Task<string> GetValuesAsync() {           

    using (var httpClient = new HttpClient()) {

        var response = await httpClient
            .GetAsync("http://www.google.com")
            .ConfigureAwait(continueOnCapturedContext: false);

        // And now we're on the thread pool thread.

        // This "await" will capture the current SynchronizationContext...
        var html = await GetStringAsync();
        // ... and resume it here.

        // But it's not the UI SynchronizationContext.
        // It's the ThreadPool SynchronizationContext.
        // So we're back on a thread pool thread here.

        // So this will raise an exception.
        html += "Hey...";
        Foo.Text = html;

        return html;
    }
}

Also, in this case, is there a chance for GetStringAsync method to be posted back to UI thread bacuse the httpClient.GetAsync method continuation may be executed inside the UI thread?

The only way GetStringAsync will run on the UI thread is if GetAsync completes before it's actually awaited. Highly unlikely.

For this reason, I prefer to use ConfigureAwait(false) for every await once the context is no longer needed.

Thursday, August 12, 2021
 
francadaval
answered 4 Months ago
79

You're not returning anything from Read_Json_File, thus you get undefined -- you're returning data from the callback which doesn't result in anything. Instead, to use async/await, you'd need to promisify fs.readFile since it's not already. Then you'll be able to use async/await:

function readJSONFile() {
  return new Promise((resolve, reject) => {
    fs.readFile('import.json', 'utf-8', (err, data) => { 
      if (err) reject(err);
      resolve(JSON.parse(data));
    });
  });
}

Await requires an actual promise to wait for. What this does is return a promise to use await on. Thus, we wait until we call resolve - which happens when we're done loading the JSON:

let json = await readJSONFile();
console.log(json);

Here we call readJSONFile. This returns a promise which resolves when the JSON file is done loading, and allows for seemingly synchronous execution of asynchronous code.

Thursday, November 18, 2021
 
nirvair
answered 2 Weeks ago
Only authorized users can answer the question. Please sign in first, or register a free account.
Not the answer you're looking for? Browse other questions tagged :  
Share