Question

How to Use async/await with Array.map in TypeScript

typescriptjavascriptpromiseasync-awaitecmascript-2017

Question

Given the following TypeScript code:

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

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

This produces the error:

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

How can this be fixed? How do async/await and Array.map() work together correctly in TypeScript?

Short Answer

By the end of this page, you will understand why Array.map() does not automatically wait for async callbacks, why it returns an array of promises, and how to correctly combine it with Promise.all() in TypeScript to get the final resolved values.

Concept

Array.map() is a synchronous array method. Its job is simple: take each item in an array, run a callback, and return a new array containing the callback results.

When the callback is marked async, it always returns a promise. That means map() does not produce number[] anymore. It produces Promise<number>[].

So this code:

arr.map(async (item) => item + 1)

returns something like:

[Promise<number>, Promise<number>, Promise<number>, ...]

That is why TypeScript complains. You are trying to assign an array of promises to a variable typed as an array of numbers.

To turn Promise<number>[] into number[], you need Promise.all().

Promise.all() takes an array of promises, waits for all of them to finish, and gives back a single promise containing an array of resolved values.

const results = await Promise.all(arr.map(async (item) => {
  await callAsynchronousOperation(item);
  return item + 1;
}));

Now the flow is correct:

  1. map() creates an array of promises.
  2. Promise.all() waits for all of them.
  3. await unwraps the final promise.
  4. results becomes number[].

This matters in real programming because many tasks are asynchronous:

  • fetching data from APIs
  • reading files
  • querying databases
  • sending emails
  • processing multiple network requests

Understanding that map() itself does not wait is essential when working with async code.

Mental Model

Think of Array.map() as a factory conveyor belt.

  • A normal callback puts finished products on the belt.
  • An async callback puts claim tickets on the belt instead of finished products.

Each claim ticket says, "A result will be ready later."

So after map(), you do not have the final products. You have tickets: Promise[].

Promise.all() is the collection desk that waits until every product is ready, then hands you the complete box of real results.

  • map() = create one result per input
  • async = each result is promised later
  • Promise.all() = wait for all promised results
Take Quiz

Syntax and Examples

Core syntax

const promises = arr.map(async (item) => {
  return someAsyncWork(item);
});

const results = await Promise.all(promises);

A shorter version:

const results = await Promise.all(
  arr.map(async (item) => {
    await someAsyncWork(item);
    return item + 1;
  })
);

Correct version of your example

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

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

Why this works

  • arr.map(...) returns Promise<number>[]
  • Promise.all(...) returns Promise<number[]>
  • await resolves that into number[]

Beginner-friendly example

async function doubleLater(n: number): Promise<number> {
  return n * 2;
}

async function run() {
  const numbers = [1, 2, 3];

  const doubled = await Promise.all(
    numbers.map(async (n) => {
      return doubleLater(n);
    })
  );

  console.log(doubled); // [2, 4, 6]
}

Even though the callback is async, Promise.all() collects the real final values.

Step by Step Execution

Consider this code:

async function addOneLater(n: number): Promise<number> {
  return n + 1;
}

async function run() {
  const arr = [1, 2, 3];

  const results = await Promise.all(
    arr.map(async (item) => {
      const value = await addOneLater(item);
      return value;
    })
  );

  console.log(results);
}

Step by step:

  1. arr is [1, 2, 3].
  2. map() starts running the callback for each item.
  3. For 1, the callback returns a Promise<number>.
  4. For 2, the callback returns another Promise<number>.
  5. For 3, the callback returns another Promise<number>.
  6. After map(), the result is roughly:
[Promise, Promise, Promise]
  1. Promise.all(...) receives that array of promises.
  2. It waits until all three promises resolve.
  3. Once resolved, it produces:
[2, 3, 4]
  1. await unwraps that final promise.
  2. results becomes a real number[].
  3. console.log(results) prints:
[2, 3, 4]

The key idea is that await does not make map() itself asynchronous. It only waits on a promise you explicitly give it.

Real World Use Cases

Fetching multiple API resources

const userIds = [1, 2, 3];

const users = await Promise.all(
  userIds.map(async (id) => {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
  })
);

Reading multiple files

const fileNames = ["a.txt", "b.txt", "c.txt"];

const contents = await Promise.all(
  fileNames.map(async (name) => {
    return readFile(name, "utf8");
  })
);

Running validation checks

const emails = ["a@test.com", "b@test.com"];

const results = await Promise.all(
  emails.map(async (email) => {
    return checkEmailAvailability(email);
  })
);

Transforming database results

A backend might fetch rows, then asynchronously enrich each row with extra data before returning the final response.

These are common cases where you want one async operation per array item and then need all final results together.

Take Quiz

Real Codebase Usage

In real projects, developers often use map() with async work in a few common patterns.

1. Parallel processing with Promise.all()

Use this when tasks are independent and can run at the same time.

const enrichedUsers = await Promise.all(
  users.map(async (user) => ({
    ...user,
    profile: await fetchProfile(user.id),
  }))
);

2. Guard clauses before async work

const results = await Promise.all(
  items.map(async (item) => {
    if (!item.enabled) {
      return null;
    }

    return processItem(item);
  })
);

3. Validation before mapping

if (items.length === 0) {
  return [];
}

const output = await Promise.all(items.map(saveItem));

4. Error handling around the whole batch

try {
  const data = await Promise.all(ids.map(loadRecord));
  return data;
} catch (error) {
  console.error("Failed to load records", error);
  throw error;
}

5. Using filter() after async mapping

Because filter() does not work directly with async callbacks either, a common pattern is:

const values = await Promise.all(items.map(checkItem));
const passed = items.filter((_, index) => values[index]);

This shows an important idea: many array helpers are synchronous, so async code often needs an extra promise step.

Common Mistakes

1. Awaiting map() directly

Broken code:

const results = await arr.map(async (item) => item + 1);

Why it is wrong:

  • map() returns an array immediately
  • await on that array does not resolve the inner promises into values

Fix:

const results = await Promise.all(
  arr.map(async (item) => item + 1)
);

2. Expecting async callbacks to return plain values

Broken code:

const values: number[] = arr.map(async (item) => item + 1);

Why it is wrong:

  • an async function always returns Promise<T>
  • so the result type is Promise<number>[]

Fix:

const values: number[] = await Promise.all(
  arr.map(async (item) => item + 1)
);

3. Forgetting error behavior of Promise.all()

If one promise rejects, Promise.all() rejects immediately.

const results = await Promise.all(tasks.map(runTask));

If that is not what you want, use Promise.allSettled() instead.

4. Using forEach with async and expecting waiting behavior

Broken code:

arr.forEach(async (item) => {
  await saveItem(item);
});
console.log("Done");

console.log("Done") may run before the saves finish.

Fix with Promise.all():

await Promise.all(arr.map(saveItem));
console.log("Done");

5. Using async callbacks with filter() or reduce() without care

Array methods like filter() are also synchronous. An async callback does not make them wait automatically.

Avoid assuming all array methods behave like await. They do not.

Comparisons

PatternWhat it returnsWaits for async work?Best use
arr.map(fn) with sync callbackT[]No needSimple transformation
arr.map(async fn)Promise<T>[]NoStart async work for each item
await Promise.all(arr.map(async fn))T[]YesParallel async transformation
for...of with awaitFinal values one by oneYes, sequentiallyWhen order or rate matters
arr.forEach(async fn)voidNoUsually avoid for awaited flows

Promise.all() vs for...of

// Parallel
const results = await Promise.all(items.map(processItem));
// Sequential
const results: Result[] = [];
for (const item of items) {
  results.push(await processItem(item));
}

Use Promise.all() when:

  • operations are independent
  • you want better performance
  • running tasks in parallel is safe

Use for...of when:

  • each step depends on the previous one
  • you must limit concurrency
  • order of completion matters
  • the API or database should not be hit all at once
Take Quiz

Cheat Sheet

// Wrong
const results = await arr.map(async (item) => item + 1);
// Right
const results = await Promise.all(
  arr.map(async (item) => item + 1)
);

Rules

  • async function always returns Promise<T>
  • Array.map() is synchronous
  • map(async ...) returns Promise<T>[]
  • Promise.all(Promise<T>[]) returns Promise<T[]>
  • await Promise.all(...) gives T[]

Common patterns

const results = await Promise.all(items.map(doAsyncWork));
const results = await Promise.all(
  items.map(async (item) => {
    const value = await doAsyncWork(item);
    return transform(value);
  })
);

When not to use Promise.all()

If tasks must run one at a time:

const results = [];
for (const item of items) {
  results.push(await doAsyncWork(item));
}

Edge case

If one promise fails, Promise.all() rejects.

Use Promise.allSettled() if you want all results regardless of failure.

FAQ

Why does Array.map() return Promise[] with an async callback?

Because an async function always returns a promise. map() collects those return values as-is.

Can I use await directly on arr.map(...)?

No. arr.map(...) returns an array, not a single promise that resolves all inner results.

What is the correct way to use async with map()?

Use Promise.all() around the map() result:

const results = await Promise.all(arr.map(asyncFn));

Is Promise.all() parallel?

It starts all mapped async operations without waiting for one another, so they can run concurrently.

What if one mapped promise fails?

Promise.all() rejects immediately with that error.

Should I use forEach instead of map()?

Not when you need the final resolved values. forEach does not return a useful result for awaited transformations.

When should I use for...of instead of Promise.all()?

Use for...of when tasks must run sequentially or when you need to avoid too many concurrent requests.

Take Quiz

Mini Project

Description

Build a small TypeScript utility that processes a list of product IDs asynchronously and returns updated prices. This demonstrates the exact pattern of using Array.map() with async callbacks and then resolving everything with Promise.all().

Goal

Create a function that asynchronously transforms an array of numbers into a new array of processed numbers.

Requirements

  • Create an async function that simulates loading a product price by ID.
  • Use Array.map() to process each ID.
  • Use Promise.all() to wait for all async operations.
  • Return a final number[].
  • Log the result to verify the output.
Take Quiz

Keep learning

Related questions

View more