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:

js
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:

js
// ❌ 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.

Wskazówka: Jeśli masz wiele wyrażeń await w jednym bloku try, pojedynczy catch obsługuje je wszystkie — ale tracisz kontekst, które z nich zawiodło. W przypadku złożonych przepływów rozważ osobne bloki try/catch lub pomocnik jak 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:

js
// ❌ 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:

js
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:

js
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 processed

Uż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:

js
// 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;
js
// 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 loaded

Await 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:

js
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.