Question
How to Use async/await with Array.map in TypeScript
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:
map()creates an array of promises.Promise.all()waits for all of them.awaitunwraps the final promise.resultsbecomesnumber[].
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
asynccallback 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 inputasync= each result is promised laterPromise.all()= wait for all promised results
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(...)returnsPromise<number>[]Promise.all(...)returnsPromise<number[]>awaitresolves that intonumber[]
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:
arris[1, 2, 3].map()starts running the callback for each item.- For
1, the callback returns aPromise<number>. - For
2, the callback returns anotherPromise<number>. - For
3, the callback returns anotherPromise<number>. - After
map(), the result is roughly:
[Promise, Promise, Promise]
Promise.all(...)receives that array of promises.- It waits until all three promises resolve.
- Once resolved, it produces:
[2, 3, 4]
awaitunwraps that final promise.resultsbecomes a realnumber[].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.
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 immediatelyawaiton 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
asyncfunction always returnsPromise<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
| Pattern | What it returns | Waits for async work? | Best use |
|---|---|---|---|
arr.map(fn) with sync callback | T[] | No need | Simple transformation |
arr.map(async fn) | Promise<T>[] | No | Start async work for each item |
await Promise.all(arr.map(async fn)) | T[] | Yes | Parallel async transformation |
for...of with await | Final values one by one | Yes, sequentially | When order or rate matters |
arr.forEach(async fn) | void | No | Usually 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
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 functionalways returnsPromise<T>Array.map()is synchronousmap(async ...)returnsPromise<T>[]Promise.all(Promise<T>[])returnsPromise<T[]>await Promise.all(...)givesT[]
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.
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.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.