Async/await and Array.map in TypeScript: 5 Deadly Mistakes

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Async/await and Array.map: When Best Friends Become Enemies



Have you ever written something like const results = await array.map(async (item) => {...}) and got an array of Promise instead of the expected data? If so — you are not alone. This question on Stack Overflow has gathered tens of thousands of views, and the number of developers who have stepped on this rake is in the millions.



In this article, we will break down 5 most dangerous mistakes when using async/await with Array.map in TypeScript. And more importantly, I will show you how to avoid them. Buckle up — it will be painful but useful.



Mistake #1: Forgetting Promise.all (The Most Popular)



The most common bug is thinking that await inside map will magically wait for all promises. Spoiler: no, it won't.



The Problem

// ❌ This doesn't workconst users = [1, 2, 3, 4, 5];const userData = await users.map(async (id) => {  const response = await fetch(`/api/users/${id}`);  return response.json();});// userData is Promise[], not UserData[]!console.log(userData); // [Promise, Promise, Promise, Promise, Promise]


Why Does This Happen?

Array.map is synchronous. It does not wait for asynchronous callbacks to complete. Each call to an async function returns a Promise, and map simply collects them into an array. await before map only waits for the map itself to finish, but not its callbacks.



The Solution

// ✅ Correct: wrap in Promise.allconst users = [1, 2, 3, 4, 5];const userData = await Promise.all(  users.map(async (id) => {    const response = await fetch(`/api/users/${id}`);    return response.json();  }));// userData is UserData[]! 🎉console.log(userData);


Mistake #2: All Promises Execute in Parallel (When Sequential is Needed)



Promise.all runs all promises in parallel. This is great for performance, but sometimes we need to execute requests strictly one after another. For example, when working with an API that limits the number of requests, or when each subsequent request depends on the previous one.



The Problem

// ❌ All requests fire simultaneouslyconst ids = [1, 2, 3, 4, 5];const results = await Promise.all(  ids.map(async (id) => {    // 5 requests to the API at once — can hit rate limit    return await api.processItem(id);  }));


The Solution: Sequential Execution

// ✅ Use for...of with awaitconst ids = [1, 2, 3, 4, 5];const results: ItemData[] = [];

for (const id of ids) { const result = await api.processItem(id); results.push(result);}// results — array with data processed in order


An alternative is Array.reduce with a promise accumulator:

// ✅ Also works, but less readableconst ids = [1, 2, 3, 4, 5];const results = await ids.reduce(async (accPromise, id) => {  const acc = await accPromise;  const result = await api.processItem(id);  return [...acc, result];}, Promise.resolve([] as ItemData[]));


Mistake #3: Incorrect Typing in TypeScript



TypeScript may not immediately understand what you are doing with an async map. Especially if you use Promise.all without explicitly specifying the type.



The Problem

// ❌ TypeScript complains or infers the wrong typeconst ids = [1, 2, 3];const results = await Promise.all(  ids.map(async (id) => {    const data = await fetchData(id);    return data.name; // TypeScript might think this is Promise<string>  }));// results has type unknown or Promise<string>[]


The Solution: Explicit Type Annotation

// ✅ Explicitly specify the result type                

Blogs

Book Recommendations