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ść.
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, to generowanie HTML na serwerze przy żądaniu — komponent client:only je pomija i renderuje się wyłącznie w przeglądarce. i integracjach z kilkoma API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami. Tu chodzi o konwencje plików Next.js, które zamieniają eksportowaną funkcję w gotowy plik sitemap.xml lub robots.txt.. 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.
Problem: sekwencyjne await
Wyobraź sobie typową funkcję, która pobiera dane z kilku źródeł:
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.
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.
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.
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ć sekwencyjneconst user = await fetchUser(userId)const orders = await fetchOrders(user.customerId) // potrzebuje user.customerIdconst 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():
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.
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łaconst data = await Promise.any([ fetchFromPrimaryAPI(), fetchFromBackupAPI(), fetchFromCache(),])
Promise.any() rzuca AggregateError tylko jeśli wszystkie Promise zostaną odrzucone.
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 razconst 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życieconst 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 operacjiconst 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łeconst parallelResults = await Promise.all(ids.map((id) => fetchData(id)))
// ❌ Źle — forEach ignoruje Promiseids.forEach(async (id) => { await processItem(id) // Te operacje "uciekają"})console.log('Done') // Wyświetli się przed zakończeniem!// ✅ Dobrzeawait 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 problemyconst allAtOnceResults = await Promise.all(ids.map((id) => fetchData(id)))// ✅ Lepiej — limit równoległościconst 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ć.
Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwija własne projekty side-projecty.
Praktyczny przewodnik po projektowaniu REST API. Konwencje URL, metody HTTP, błędy, wersjonowanie, paginacja i kilka ważnych niuansów, które zwykle pomija się w prostych tutorialach.