Async/await to pułapka — kiedy Promise.all() uratuje Twoją wydajność

Sekwencyjne await potrafią niepotrzebnie wydłużyć czas odpowiedzi aplikacji. Zobacz, kiedy użyć Promise.all(), kiedy Promise.allSettled(), a kiedy ograniczyć równoległość.

Opublikowano

2 listopada 2025 11:38

Czytanie

6 min czytania

Aktualizacja

15 kwietnia 2026 11:52

async/await zrobił dla czytelności JavaScript więcej niż większość nowych składni razem wziętych. Po jego wprowadzeniu, kod wygląda liniowo, łatwiej go prześledzić i trudniej zgubić się w łańcuchu callbacków. Z drugiej strony problem w tym, że bardzo łatwo napisać kod, który wygląda dobrze, a jednocześnie niepotrzebnie serializuje pracę.

Samo await nie jest pułapką, tylko ustawienie kilku await jeden po drugim wtedy, gdy operacje są od siebie niezależne i mogłyby wystartować równolegle.

W praktyce ten błąd pojawia się w dashboardach, loaderach danych, SSR, czyli Server-Side Rendering, oznacza generowanie HTML na serwerze przy każdym żądaniu. i integracjach z kilkoma API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami.. Każdy request jest poprawny, ale całość trwa dłużej, niżeli musi. W tym artykule pokażę, jak to rozpoznać, kiedy Promise.all() faktycznie pomaga, a kiedy lepiej zostać przy sekwencji albo wprowadzić limit równoległości. Jeśli chcesz najpierw zrozumieć, jak JavaScript zarządza asynchronicznością pod spodem, sprawdź artykuł o pętli zdarzeń.

Krótka odpowiedź: Niezależne operacje (user + posts + stats) → Promise.all() (nawet 3x szybciej). Tolerancja błędów → Promise.allSettled(). Wiele operacji na dużym zbiorze → batching z limitem. Sekwencyjne await zostaw tylko gdy wynik jednej operacji jest potrzebny do kolejnej.

Problem: sekwencyjne await

Wyobraź sobie typową funkcję, która pobiera dane z kilku źródeł:

Code
async function getDashboardData(userId) {
  const user = await fetchUser(userId) // 200ms
  const posts = await fetchPosts(userId) // 300ms
  const notifications = await fetchNotifications(userId) // 150ms
  const stats = await fetchStats(userId) // 250ms
 
  return { user, posts, notifications, stats }
}

W takim zapisie każda kolejna operacja startuje dopiero wtedy, gdy poprzednia się zakończy i nie jest to wada async/await jako takiego tylko po prostu efekt sekwencyjnego ustawienia await. Pytanie brzmi: ile czasu zajmie wykonanie? Około 900ms plus narzut środowiska, czyli sumujesz czas wszystkich operacji, bo każda czeka na poprzednią.

Ale te zapytania są niezależne, ponieważ nie potrzebujesz wyniku fetchUser() żeby wywołać fetchPosts(), które mogłoby wystartować od razu.

Rozwiązanie: Promise.all()

Code
async function getDashboardData(userId) {
  const [user, posts, notifications, stats] = await Promise.all([
    fetchUser(userId), // 200ms
    fetchPosts(userId), // 300ms
    fetchNotifications(userId), // 150ms
    fetchStats(userId), // 250ms
  ])
 
  return { user, posts, notifications, stats }
}

Ile czasu teraz? 300ms — tyle ile najwolniejsze zapytanie.

Właśnie o to chodzi w Promise.all(), by nie przyspieszać pojedynczych requestów, ale po prostu uruchamiasz wszystkie niezależne operacje jak najwcześniej i czekasz na komplet wyników. W tym przykładzie dostajesz prawie 3x szybciej i to bez zmiany logiki biznesowej albo niepotrzebnego grzebania w backendzie.

Benchmark: realna różnica

Na prostym benchmarku różnica wygląda tak:

Code
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
 
async function sequential() {
  await delay(200)
  await delay(300)
  await delay(150)
  await delay(250)
}
 
async function parallel() {
  await Promise.all([delay(200), delay(300), delay(150), delay(250)])
}
 
// Pomiar
console.time('sequential')
await sequential()
console.timeEnd('sequential') // ~900ms
 
console.time('parallel')
await parallel()
console.timeEnd('parallel') // ~300ms

W realnym świecie wynik bywa podobny albo jeszcze bardziej odczuwalny, bo operacje sieciowe mają dodatkowy narzut: połączenie, TLS, kolejki, czas serwera i opóźnienia między regionami.

Kiedy używać Promise.all()?

Używaj gdy operacje są niezależne:

Code
// ✅ Niezależne — mogą działać równolegle
const [user, products, cart] = await Promise.all([
  fetchUser(userId),
  fetchProducts(categoryId),
  fetchCart(userId),
])

Najprostsza heurystyka jest taka: jeśli potrafisz przygotować wszystkie wywołania przed pierwszym await, jest duża szansa, że możesz wykonać je równolegle.

Nie używaj gdy wynik jednej operacji jest potrzebny do drugiej:

Code
// ❌ Zależne — muszą być sekwencyjne
const user = await fetchUser(userId)
const orders = await fetchOrders(user.customerId) // potrzebuje user.customerId
const payments = await fetchPayments(orders[0].id) // potrzebuje orders

Jeśli część operacji jest zależna, a część nie, rozdziel to na etapy. Najpierw pobierz to, od czego zależą kolejne kroki, a dopiero potem odpal równoległość.

Drugi ważny warunek: niezależność to nie wszystko. Jeśli masz setki albo tysiące zadań, Promise.all() bez limitu może skończyć się rate limitingiem, problemami z pamięcią albo przeciążeniem API. Wtedy lepszy będzie batching albo pool, o których za chwilę.

Obsługa błędów: Promise.all vs Promise.allSettled

Promise.all() ma zachowanie fail-fast: pierwszy odrzucony Promise odrzuca całą agregację.

Code
try {
  const [a, b, c] = await Promise.all([
    fetchA(), // OK
    fetchB(), // FAIL — rzuca błąd
    fetchC(), // nadal się wykonuje — wynik jest tylko ignorowany
  ])
} catch (error) {
  // catch dostaje błąd z pierwszej odrzuconej Promise
  // nie dostajesz częściowych wyników w prosty sposób
  // pozostałe operacje dalej wykonują się w tle, chyba że same wspierają anulowanie
}

To bardzo dobre zachowanie wtedy, gdy komplet danych jest wymagany. Jeśli dashboard bez jednej sekcji i tak nie ma sensu, fail-fast upraszcza obsługę błędów.

Jeśli chcesz kontynuować mimo błędów, użyj Promise.allSettled():

Code
const results = await Promise.allSettled([
  fetchA(),
  fetchB(), // może się nie powieść
  fetchC(),
])
 
// results = [
//   { status: 'fulfilled', value: resultA },
//   { status: 'rejected', reason: Error },
//   { status: 'fulfilled', value: resultC },
// ]
 
// Wyciągnij tylko udane
const successful = results
  .filter((r) => r.status === 'fulfilled')
  .map((r) => r.value)
 
// Wyciągnij błędy do logowania
const errors = results
  .filter((r) => r.status === 'rejected')
  .map((r) => r.reason)

Promise.allSettled() jest świetne do sytuacji, w których częściowy sukces nadal ma wartość: widgety dashboardu, kilka źródeł danych, logowanie błędów bez blokowania całej strony.

Promise.race() i Promise.any()

Dwa inne przydatne narzędzia:

Promise.race() — pierwszy wynik (sukces lub błąd)

Code
// Generic timeout wrapper
function withTimeout(promise, ms) {
  return Promise.race([
    promise,
    new Promise((_, reject) =>
      setTimeout(() => reject(new Error('Timeout')), ms),
    ),
  ])
}
 
const data = await withTimeout(fetchData(), 5000)

To dobry wzorzec dla timeoutów, ale pamiętaj: Promise.race() nie anuluje przegranego zadania. Jeśli timeout ma faktycznie przerwać fetch, użyj AbortController.

Promise.any() — pierwszy sukces (ignoruje błędy)

Code
// Fallback pattern — użyj pierwszego działającego źródła
const data = await Promise.any([
  fetchFromPrimaryAPI(),
  fetchFromBackupAPI(),
  fetchFromCache(),
])

Promise.any() rzuca AggregateError tylko jeśli wszystkie Promise zostaną odrzucone.

Wzorce dla złożonych scenariuszy

Wzorzec 1: Częściowa równoległość

Gdy masz miks zależnych i niezależnych operacji:

Code
async function getOrderDetails(orderId) {
  // Krok 1: Pobierz zamówienie
  const order = await fetchOrder(orderId)
 
  // Krok 2: Równolegle pobierz powiązane dane
  const [customer, products, shipping] = await Promise.all([
    fetchCustomer(order.customerId),
    fetchProducts(order.productIds),
    fetchShipping(order.shippingId),
  ])
 
  return { order, customer, products, shipping }
}

Wzorzec 2: Batch processing z limitem równoległości

Gdy masz dużo operacji i nie chcesz przeciążyć serwera:

Code
async function processBatch(items, fn, concurrency = 5) {
  const results = []
 
  for (let i = 0; i < items.length; i += concurrency) {
    const batch = items.slice(i, i + concurrency)
    const batchResults = await Promise.all(batch.map(fn))
    results.push(...batchResults)
  }
 
  return results
}
 
// Użycie: przetwórz 100 elementów, max 5 na raz
const users = await processBatch(userIds, fetchUser, 5)

To nadal jest sekwencja, ale na poziomie batchy. Wewnątrz każdej paczki operacje lecą równolegle, dzięki czemu ograniczasz obciążenie bez całkowitej utraty wydajności.

Wzorzec 3: Retry z exponential backoff

Code
async function fetchWithRetry(fn, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn()
    } catch (error) {
      if (i === maxRetries - 1) throw error
 
      const delay = Math.pow(2, i) * 1000 // 1s, 2s, 4s
      await new Promise((r) => setTimeout(r, delay))
    }
  }
}
 
// Użycie
const data = await fetchWithRetry(async () => {
  const response = await fetch('/api/flaky-endpoint')
 
  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`)
  }
 
  return response.json()
})

Ważny detal: fetch() nie odrzuca Promise przy 404 czy 500, więc jeśli chcesz retry dla błędów HTTP, musisz rzucić wyjątek samodzielnie. Retry ma też największy sens dla operacji idempotentnych, zwłaszcza GET.

Wzorzec 4: Promise pool (zaawansowany)

Dla bardzo dużych zbiorów danych, gdzie nawet batching nie wystarczy:

Code
async function promisePool(items, fn, concurrency) {
  const results = new Array(items.length)
  let index = 0
 
  async function worker() {
    while (index < items.length) {
      const currentIndex = index++
      results[currentIndex] = await fn(items[currentIndex])
    }
  }
 
  await Promise.all(Array.from({ length: concurrency }, worker))
 
  return results
}
 
// 1000 elementów, max 10 równoległych operacji
const results = await promisePool(largeArray, processItem, 10)

W realnym projekcie często wygodniej użyć gotowego narzędzia typu p-limit albo p-map, ale dobrze rozumieć, co dzieje się pod spodem.

Typowe błędy

Błąd 1: Sekwencyjny await w pętli

Code
// ❌ Źle — każda iteracja czeka na poprzednią
const sequentialResults = []
 
for (const id of ids) {
  const data = await fetchData(id)
  sequentialResults.push(data)
}
 
// ✅ Dobrze — równoległe
const parallelResults = await Promise.all(ids.map((id) => fetchData(id)))

Błąd 2: Await w map bez Promise.all

Code
// ❌ Źle — await na tablicy NIE rozwiązuje Promise w środku
const pendingResults = await ids.map((id) => fetchData(id))
// pendingResults = [Promise, Promise, Promise...]
 
// ✅ Dobrze
const resolvedResults = await Promise.all(ids.map((id) => fetchData(id)))
// resolvedResults = [data, data, data...]

Błąd 3: forEach z async

Code
// ❌ Źle — forEach ignoruje Promise
ids.forEach(async (id) => {
  await processItem(id) // Te operacje "uciekają"
})
console.log('Done') // Wyświetli się przed zakończeniem!
 
// ✅ Dobrze
await Promise.all(ids.map((id) => processItem(id)))
console.log('Done') // Teraz czeka na wszystkie

Błąd 4: Nieograniczone Promise.all()

Code
// ❌ Ryzykowne — 2000 requestów naraz to proszenie się o problemy
const allAtOnceResults = await Promise.all(ids.map((id) => fetchData(id)))
 
// ✅ Lepiej — limit równoległości
const limitedResults = await processBatch(ids, fetchData, 10)

Promise.all() nie ma wbudowanego limitu i dla małej liczby operacji to świetne rozwiązanie, choć dla dużej liczby zadań może pogorszyć stabilność zamiast poprawić.

Debugowanie: mierzenie czasu

Code
async function withTiming(name, fn) {
  const start = performance.now()
  const result = await fn()
  const duration = performance.now() - start
  console.log(`${name}: ${duration.toFixed(2)}ms`)
  return result
}
 
// Użycie
const data = await withTiming('Dashboard data', () =>
  Promise.all([fetchUser(), fetchPosts(), fetchStats()]),
)

FAQ

Kiedy używać Promise.all zamiast sekwencyjnych await?

Gdy operacje są od siebie niezależne — wynik jednej nie jest potrzebny do uruchomienia kolejnej. Typowe przypadki: pobieranie danych z wielu endpointów dla jednej strony (user + posty + statystyki), równoległe zapytania do bazy, wiele niezależnych walidacji. Prosta heurystyka: jeśli możesz zapisać wszystkie wywołania przed pierwszym await, prawdopodobnie możesz uruchomić je równolegle. Efekt: czas = najwolniejsza operacja zamiast suma wszystkich.

Czym różni się Promise.allSettled od Promise.all?

Promise.all() działa w trybie fail-fast — pierwsza odrzucona Promise powoduje odrzucenie całej agregacji. Promise.allSettled() zawsze czeka na wszystkie Promise i zwraca tablicę obiektów ze statusem (fulfilled/rejected) i wynikiem lub błędem. Użyj allSettled gdy częściowy sukces ma wartość: widgety dashboardu mogą się ładować niezależnie, błąd jednego nie powinien blokować pozostałych.

Jak poprawnie obsługiwać błędy w Promise.all?

Owiń w try/catch: przy błędzie dostajesz wyjątek z pierwszego odrzuconego Promise. Pozostałe Promise nadal wykonują się w tle — Promise.all nie anuluje ich automatycznie. Jeśli chcesz anulować operacje przy błędzie, użyj AbortController. Jeśli chcesz częściowe wyniki mimo błędów, użyj Promise.allSettled() zamiast Promise.all().

Jaka różnica między Promise.race a Promise.any?

Promise.race() rozwiązuje się pierwszym wynikiem niezależnie od tego, czy to sukces czy błąd. Przydatne do implementacji timeoutów: Promise.race([fetch(...), timeoutPromise]). Promise.any() ignoruje błędy i rozwiązuje się pierwszym sukcesem — rzuca AggregateError tylko gdy wszystkie Promise zostaną odrzucone. Przydatne do pattern fallback: użyj pierwszego działającego źródła danych spośród kilku.

Jak ograniczyć równoległość async operacji?

Batching: podziel tablicę na kawałki i przetwarzaj każdy przez Promise.all() — sekwencja batchów, równoległość wewnątrz batcha. Pool (zaawansowany): utrzymuj stałą liczbę równoległych workerów. Biblioteki jak p-limit lub p-map robią to gotowo. Ważne: Promise.all(1000items.map(fetch)) bez limitu może skutkować rate limitingiem, wyczerpaniem pamięci lub przeciążeniem serwera.

Czy async/await jest wolniejszy od .then()?

Nie — async/await to sugar syntax nad Promise, kompiluje się do identycznego kodu. Problem z wydajnością nie leży w samej składni, lecz w tym, że await blokuje dalsze wykonanie w danej funkcji, co łatwo prowadzi do nieświadomej serializacji niezależnych operacji. Przy .then() te same błędy można popełnić równie łatwo — składnia nie chroni przed złym projektem przepływu asynchronicznego.

Dlaczego forEach z async nie działa poprawnie?

Array.prototype.forEach nie obsługuje asynchronicznych callbacków — wywołuje callback, nie czeka na Promise i idzie dalej. W efekcie await wewnątrz forEach działa w izolowanym kontekście i "ucieka" spod kontroli zewnętrznej funkcji. Kod po forEach wykonuje się zanim operacje wewnątrz się skończą. Zamiast forEach z async: await Promise.all(arr.map(async item => ...)) dla równoległości lub for...of z await dla sekwencji.

Źródła i dokumentacja

Podsumowanie

Zbierając wszystkie informacje, podsumujmy:

ScenariuszRozwiązanie
Niezależne operacjePromise.all()
Niezależne, toleruj błędyPromise.allSettled()
Pierwszy sukcesPromise.any()
Pierwszy wynik lub timeoutPromise.race()
Miks zależnych i niezależnychEtapy + Promise.all()
Wiele operacji + limitBatching, pool lub limiter

Najważniejsza zasada nie będzie tutaj brzmiała: "zawsze używaj Promise.all()", ale raczej: uruchamiaj niezależne operacje jak najwcześniej, a czekaj na nie dopiero wtedy, gdy naprawdę potrzebujesz wyniku tych operacji.

async/await sprawia, że sekwencyjny kod pisze się bardzo naturalnie, i generalnie dlatego tak łatwo przeoczyć miejsca, w których aplikacja niepotrzebnie stoi.

Musisz sobie zadać jedno proste pytanie przy każdym await:

"Czy ta operacja naprawdę musi czekać na poprzednią?" i jeśli tak, to rób to.


Chcesz pójść dalej? Sprawdź 10 trików JavaScript dla czytelniejszego kodu albo zobacz, jak debugować JavaScript bez zasypywania kodu console.logami.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab