Jeśli piszesz JavaScript od kilku miesięcy, na pewno odczułeś ból głęboko zagnieżdżonych callbacków i splątanych łańcuchów .then(). async/await, wprowadzone w ES2017, rozwiązało ten problem — a jednak programiści wciąż wpadają w te same trzy lub cztery pułapki. Przejdźmy przez to wszystko porządnie: jak to działa, jak dobrze obsługiwać błędy i jakie wzorce równoległego wykonywania naprawdę mają znaczenie dla wydajności.
Podstawy — funkcje async i await
Funkcja async zawsze zwraca Promise. Wewnątrz niej await wstrzymuje wykonanie do momentu rozliczenia oczekiwanego Promise. To cały model mentalny:
async function fetchUserProfile(userId) {
const response = await fetch(`https://api.example.com/users/${userId}`);
const profile = await response.json();
return profile; // wrapped in a Promise automatically
}
// Calling an async function gives you a Promise
const profilePromise = fetchUserProfile(42);
profilePromise.then(profile => console.log(profile.name));
// Or use await at the call site
const profile = await fetchUserProfile(42);
console.log(profile.name);Słowo kluczowe await może być używane wyłącznie wewnątrz funkcji async — lub na najwyższym poziomie modułu ES (więcej o tym później). Użycie go gdziekolwiek indziej jest błędem składni.
Obsługa błędów — właściwy i niewłaściwy sposób
Tutaj większość poradników schodzi na złą drogę. Instynkt podpowiada, by owinąć wszystko w try/catch i uznać sprawę za zamkniętą. To działa, ale puste bloki catch to zapach kodu, który maskuje prawdziwe błędy:
// ❌ Don't do this — silent failure, impossible to debug
async function loadConfig() {
try {
const res = await fetch('/api/config');
return await res.json();
} catch (err) {
// swallowed — you'll never know what broke
}
}
// ✅ Do this — handle errors explicitly, return something meaningful
async function loadConfig() {
try {
const res = await fetch('/api/config');
if (!res.ok) {
throw new Error(`Config fetch failed: ${res.status} ${res.statusText}`);
}
return await res.json();
} catch (err) {
console.error('loadConfig error:', err.message);
return null; // caller can check for null
}
}Preferuję wzorzec, w którym funkcje async zwracają null w przypadku błędu albo rzucają celowe wyjątki. Czego unikam, to przechwytywanie błędu, logowanie go, a następnie zwracanie wartości, która sprawia, że wywołujący myśli, że żądanie się powiodło.
await to(promise), który zwraca krotki [error, data].Sekwencyjnie vs równolegle — pułapka wydajności
To błąd, który najczęściej widzę w kodzie produkcyjnym. Gdy używasz await dla każdego wywołania po kolei, wykonujesz je sekwencyjnie — nawet jeśli są całkowicie od siebie niezależne:
// ❌ Sequential — takes ~900ms total (300 + 300 + 300)
async function loadDashboard(userId) {
const user = await fetchUser(userId); // 300ms
const orders = await fetchOrders(userId); // 300ms
const settings = await fetchSettings(userId); // 300ms
return { user, orders, settings };
}
// ✅ Parallel with Promise.all — takes ~300ms total
async function loadDashboard(userId) {
const [user, orders, settings] = await Promise.all([
fetchUser(userId),
fetchOrders(userId),
fetchSettings(userId)
]);
return { user, orders, settings };
}Promise.all() uruchamia wszystkie trzy żądania w tym samym momencie i czeka, aż wszystkie się zakończą. Jeśli którekolwiek z nich zostanie odrzucone, całość zostaje odrzucona. Zwykle to właśnie chcemy przy ładowaniu dashboardu, gdzie wszystkie dane są wymagane.
Promise.allSettled — gdy częściowe niepowodzenie jest dopuszczalne
Czasem chcesz wysłać wiele żądań i wykorzystać to, co wróciło poprawnie, nawet jeśli niektóre zawiodły. Promise.allSettled() jest stworzony dokładnie do tego:
async function loadWidgets(widgetIds) {
const results = await Promise.allSettled(
widgetIds.map(id => fetchWidget(id))
);
const widgets = [];
const errors = [];
for (const result of results) {
if (result.status === 'fulfilled') {
widgets.push(result.value);
} else {
errors.push(result.reason.message);
}
}
if (errors.length > 0) {
console.warn('Some widgets failed to load:', errors);
}
return widgets; // return whatever succeeded
}Ten wzorzec sprawdza się doskonale przy niekrytycznych elementach UI — jak pasek boczny z wieloma niezależnymi sekcjami. Jeśli jedna zawiedzie, pokazujesz pozostałe zamiast wygaszać całą stronę.
async w pętlach — pułapka forEach
To przynajmniej raz dotknęło każdego. Array.forEach() nie czeka na asynchroniczne callbacki — uruchamia je i natychmiast kontynuuje. Pętla kończy się, zanim jakakolwiek praca asynchroniczna zostanie ukończona:
const orderIds = [101, 102, 103, 104];
// ❌ forEach ignores async — all requests fire in parallel uncontrolled,
// and code after the forEach runs before any complete
orderIds.forEach(async (id) => {
await processOrder(id); // NOT awaited by forEach
});
console.log('done?'); // prints before any order is processed
// ✅ for...of — sequential, fully awaited
for (const id of orderIds) {
await processOrder(id);
}
console.log('done'); // prints after all orders are processed
// ✅ Parallel but controlled — all fire at once, await all completions
await Promise.all(orderIds.map(id => processOrder(id)));
console.log('done'); // prints after all orders are processedUżywaj for...of, gdy kolejność ma znaczenie lub gdy chcesz ograniczać żądania (przetwarzaj jedno na raz). Używaj Promise.all(map(...)), gdy chcesz maksymalnego równoległości i nie potrzebujesz gwarancji sekwencyjności.
Await na najwyższym poziomie w modułach ES
Od ES2022 możesz używać await na najwyższym poziomie modułu ES — bez potrzeby tworzenia funkcji opakowującej. To duże ułatwienie przy inicjalizacji modułów zależnych od danych asynchronicznych:
// config.js (ES module)
const response = await fetch('/api/runtime-config');
const config = await response.json();
export const API_BASE_URL = config.apiBaseUrl;
export const FEATURE_FLAGS = config.featureFlags;// main.js — imports wait for config.js to fully resolve
import { API_BASE_URL, FEATURE_FLAGS } from './config.js';
console.log(API_BASE_URL); // guaranteed to be loadedAwait na najwyższym poziomie działa w Node.js 14.8+ z "type": "module" w package.json oraz we wszystkich nowoczesnych przeglądarkach poprzez natywne moduły ES. Wykonywanie importującego modułu jest wstrzymane do momentu pełnego rozwiązania oczekiwanego modułu — to dokładnie ta gwarancja, której potrzebujesz.
Realistyczny potok — pobieranie, parsowanie i transformacja
Oto realistyczny potok asynchroniczny łączący wszystko: pobieranie danych z API, obsługę błędów HTTP, transformację danych i eleganckie awaryjne zachowanie przy błędach:
async function getProductCatalog(categoryId) {
// Step 1: fetch raw data
const response = await fetch(
`https://api.shop.example.com/categories/${categoryId}/products`,
{ headers: { Authorization: `Bearer ${getAuthToken()}` } }
);
if (!response.ok) {
throw new Error(`Catalog fetch failed: ${response.status}`);
}
// Step 2: parse JSON
const raw = await response.json();
// Step 3: transform into the shape your UI needs
const products = raw.items.map(item => ({
id: item.product_id,
name: item.display_name,
price: (item.price_cents / 100).toFixed(2),
inStock: item.inventory_count > 0,
imageUrl: item.media?.[0]?.url ?? '/images/placeholder.png'
}));
// Step 4: filter out anything that's been discontinued
return products.filter(p => !p.discontinued);
}
// Usage
try {
const catalog = await getProductCatalog('electronics');
renderProductGrid(catalog);
} catch (err) {
showErrorBanner(`Could not load products: ${err.message}`);
}Przydatne narzędzia
Podczas debugowania kodu asynchronicznego przetwarzającego dane JSON te narzędzia są pomocne: JSON Formatter do inspekcji odpowiedzi API, JSON Validator do wychwytywania zniekształconych danych przed ich dotarciem do kodu oraz JS Formatter do porządkowania kodu funkcji asynchronicznych. Kompletną specyfikację async/await znajdziesz w przewodniku MDN po async, a specyfikacja TC39 opisuje dokładną semantykę, jeśli jej potrzebujesz.
Podsumowanie
async/await sprawia, że asynchroniczny JavaScript jest czytelny — ale pułapki są realne. Zawsze sprawdzaj response.ok przed parsowaniem, nigdy nie pozwól blokom catch na ciche połykanie błędów, używaj Promise.all() do niezależnych równoległych wywołań i trzymaj się z daleka od forEach z asynchronicznymi callbackami. Wyrób sobie te nawyki, a Twój kod asynchroniczny będzie zarówno szybki, jak i łatwy do debugowania.