Lekcja 16: Asynchroniczny JavaScript - Async/Await
W poprzednich lekcjach poznaliśmy callbacki i obietnice (Promises) jako sposoby zarządzania asynchronicznością w JavaScript. Chociaż obietnice znacznie poprawiły czytelność w porównaniu do "Callback Hell", ES2017 (ES8) wprowadziło jeszcze bardziej elegancką składnię do pracy z obietnicami: async
i await
.
async/await
to "cukier syntaktyczny" (syntactic sugar) zbudowany na bazie obietnic, który pozwala pisać kod asynchroniczny w sposób, który wygląda i zachowuje się bardziej jak kod synchroniczny, co znacznie poprawia jego czytelność i ułatwia rozumienie przepływu sterowania.
Funkcje async
Słowo kluczowe async
umieszczone przed definicją funkcji (async function mojaFunkcja() {...}
lub const mojaFunkcja = async () => {...}
) robi dwie rzeczy:
- Automatycznie zwraca obietnicę: Funkcja oznaczona jako
async
zawsze zwraca obietnicę. Jeśli funkcja jawnie zwróci wartość (np.return 10;
), ta wartość stanie się wynikiem spełnionej (fulfilled) obietnicy. Jeśli funkcja rzuci błąd, obietnica zostanie odrzucona (rejected) z tym błędem. - Umożliwia użycie `await` wewnątrz: Tylko wewnątrz funkcji oznaczonych jako
async
można używać słowa kluczowegoawait
.
// Definicja funkcji async
async function pobierzWartosc() {
console.log("Wewnątrz funkcji async");
// Zwrócenie wartości - obietnica zostanie spełniona z tą wartością
return 42;
}
async function rzucBlad() {
console.log("Wewnątrz funkcji async rzucającej błąd");
// Rzucenie błędu - obietnica zostanie odrzucona z tym błędem
throw new Error("Celowy błąd w funkcji async");
}
// Wywołanie funkcji async zwraca Promise
const obietnicaWartosci = pobierzWartosc();
console.log("Wynik wywołania pobierzWartosc():", obietnicaWartosci); // Promise {: 42}
obietnicaWartosci.then(wartosc => {
console.log("Otrzymana wartość z async function:", wartosc); // 42
});
const obietnicaBledu = rzucBlad();
console.log("Wynik wywołania rzucBlad():", obietnicaBledu); // Promise {: Error...}
obietnicaBledu.catch(blad => {
console.error("Złapany błąd z async function:", blad.message);
});
Operator await
Słowo kluczowe await
może być używane tylko wewnątrz funkcji async
. Umieszcza się je przed wywołaniem funkcji zwracającej obietnicę (lub bezpośrednio przed obietnicą).
await
powoduje, że wykonanie funkcji async
zostaje wstrzymane (w sposób nieblokujący głównego wątku!) do momentu, aż obietnica, na którą oczekuje, zostanie rozwiązana (spełniona lub odrzucona).
- Jeśli obietnica zostanie spełniona (fulfilled),
await
zwraca jej wynik. - Jeśli obietnica zostanie odrzucona (rejected),
await
rzuca błąd (który można złapać za pomocątry...catch
).
// Użyjemy funkcji zwracającej Promise z poprzedniej lekcji
function symulujPobieranieDanych(czySukces, opoznienie = 1000) {
return new Promise((resolve, reject) => {
console.log(`Rozpoczynam operację (opóźnienie ${opoznienie}ms)...`);
setTimeout(() => {
if (czySukces) {
const dane = `Dane pobrane po ${opoznienie}ms`;
console.log(`Operacja zakończona sukcesem (${opoznienie}ms).`);
resolve(dane);
} else {
const blad = new Error(`Błąd operacji (${opoznienie}ms)!`);
console.error(`Operacja zakończona błędem (${opoznienie}ms).`);
reject(blad);
}
}, opoznienie);
});
}
// Funkcja async używająca await
async function przetworzDane() {
console.log("Rozpoczynam przetwarzanie danych...");
// Oczekiwanie na pierwszą operację
console.log("Krok 1: Pobieranie danych A...");
const daneA = await symulujPobieranieDanych(true, 1500);
console.log("Otrzymano dane A:", daneA);
// Oczekiwanie na drugą operację
console.log("Krok 2: Pobieranie danych B...");
const daneB = await symulujPobieranieDanych(true, 500);
console.log("Otrzymano dane B:", daneB);
console.log("Przetwarzanie zakończone.");
return { wynikA: daneA, wynikB: daneB };
}
console.log("Przed wywołaniem przetworzDane()");
// Wywołanie funkcji async i obsługa jej wyniku (który jest Promise)
przetworzDane()
.then(wynikKoncowy => {
console.log("Końcowy wynik przetwarzania:", wynikKoncowy);
})
.catch(blad => {
// Ten catch jest na zewnątrz funkcji async, łapie błędy
// które nie zostały obsłużone wewnątrz przez try...catch
console.error("Nieobsłużony błąd w przetworzDane():", blad.message);
});
console.log("Po wywołaniu przetworzDane() (ale przed zakończeniem operacji wewnątrz)");
/* Oczekiwany output (kolejność logów może się nieznacznie różnić):
Przed wywołaniem przetworzDane()
Rozpoczynam przetwarzanie danych...
Krok 1: Pobieranie danych A...
Rozpoczynam operację (opóźnienie 1500ms)...
Po wywołaniu przetworzDane() (ale przed zakończeniem operacji wewnątrz)
(po 1.5s)
Operacja zakończona sukcesem (1500ms).
Otrzymano dane A: Dane pobrane po 1500ms
Krok 2: Pobieranie danych B...
Rozpoczynam operację (opóźnienie 500ms)...
(po 0.5s)
Operacja zakończona sukcesem (500ms).
Otrzymano dane B: Dane pobrane po 500ms
Przetwarzanie zakończone.
Końcowy wynik przetwarzania: { wynikA: 'Dane pobrane po 1500ms', wynikB: 'Dane pobrane po 500ms' }
*/
Kod wewnątrz funkcji przetworzDane
wygląda prawie jak kod synchroniczny, mimo że wykonuje operacje asynchroniczne jedna po drugiej.
Obsługa błędów z try...catch
Gdy używamy await
, odrzucona obietnica powoduje rzucenie błędu w miejscu wywołania await
. Aby obsłużyć takie błędy wewnątrz funkcji async
, używamy standardowego bloku try...catch
.
async function przetworzDaneZObslugaBledu() {
console.log("Rozpoczynam przetwarzanie z obsługą błędu...");
try {
console.log("Krok 1 (try): Pobieranie danych A...");
const daneA = await symulujPobieranieDanych(true, 1000);
console.log("Otrzymano dane A (try):", daneA);
console.log("Krok 2 (try): Pobieranie danych B (symulacja błędu)...");
// Ta operacja się nie uda
const daneB = await symulujPobieranieDanych(false, 500);
// Ten log się nie wykona, bo await rzuci błąd
console.log("Otrzymano dane B (try):", daneB);
// Ten kod również się nie wykona
console.log("Dalsze przetwarzanie w try...");
return { wynikA: daneA, wynikB: daneB };
} catch (blad) {
// Błąd z await zostanie złapany tutaj
console.error("Wystąpił błąd w bloku try...catch:", blad.message);
// Możemy tu zwrócić wartość domyślną lub rzucić błąd dalej
return { blad: blad.message }; // Zwracamy obiekt z informacją o błędzie
// throw blad; // Alternatywnie, rzucamy błąd dalej, aby złapał go zewnętrzny .catch()
} finally {
// Ten blok wykona się zawsze, po try lub catch
console.log("Blok finally - zakończenie przetwarzania (zawsze).");
}
}
przetworzDaneZObslugaBledu()
.then(wynik => {
console.log("Wynik funkcji z try...catch:", wynik);
})
.catch(blad => {
// Ten catch złapie błąd, jeśli został rzucony dalej z bloku catch wewnątrz funkcji
console.error("Zewnętrzny catch złapał błąd:", blad.message);
});
Porównanie: Promises vs Async/Await
Zobaczmy, jak wyglądałby przykład łańcuchowania obietnic z poprzedniej lekcji przy użyciu async/await
.
// Funkcje krokA, krokB, krokC zwracające Promise (jak w lekcji 15)
function krokA(wartosc) { /* ... zwraca Promise ... */ }
function krokB(wartosc) { /* ... zwraca Promise ... */ }
function krokC(wartosc) { /* ... zwraca Promise ... */ }
// Wersja z .then()/.catch()
/*
krokA(10)
.then(wynikA => krokB(wynikA))
.then(wynikB => krokC(wynikB))
.then(wynikC => {
console.log("Wynik końcowy (.then):", wynikC);
})
.catch(blad => {
console.error("Błąd (.then):", blad.message);
});
*/
// Wersja z async/await
async function wykonajKroki(wartoscPoczatkowa) {
try {
console.log("Start (async/await)...");
const wynikA = await krokA(wartoscPoczatkowa);
console.log("Po kroku A (async/await):", wynikA);
const wynikB = await krokB(wynikA);
console.log("Po kroku B (async/await):", wynikB);
const wynikC = await krokC(wynikB);
console.log("Wynik końcowy (async/await):", wynikC);
return wynikC;
} catch (blad) {
console.error("Błąd (async/await):", blad.message);
// Można tu obsłużyć błąd lub rzucić go dalej
throw blad;
}
}
// Wywołanie funkcji async
wykonajKroki(10)
.then(finalnyWynik => {
console.log("Funkcja async zakończona sukcesem, wynik:", finalnyWynik);
})
.catch(ostatecznyBlad => {
console.error("Funkcja async zakończona błędem:", ostatecznyBlad.message);
});
Wersja z async/await
jest często uważana za bardziej czytelną i łatwiejszą do zrozumienia, ponieważ przypomina standardowy kod synchroniczny z obsługą błędów za pomocą try...catch
.
Zadanie praktyczne
Użyj funkcji pobierzDaneUzytkownikaPromise(id)
z poprzedniej lekcji (zwracającej Promise). Napisz funkcję async
o nazwie wyswietlDaneLubBlad(id)
, która:
- Wywoła
pobierzDaneUzytkownikaPromise(id)
używającawait
. - Umieści wywołanie
await
w blokutry...catch
. - W bloku
try
, jeśli pobieranie się uda, wyświetli dane użytkownika w konsoli. - W bloku
catch
, jeśli wystąpi błąd, wyświetli komunikat o błędzie w konsoli.
Przetestuj funkcję wyswietlDaneLubBlad
zarówno z poprawnym, jak i niepoprawnym ID.
Pokaż rozwiązanie
// Założenie: funkcja pobierzDaneUzytkownikaPromise(id) istnieje i zwraca Promise
function pobierzDaneUzytkownikaPromise(id) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id > 0) {
resolve({ id: id, imie: "Test User", email: "test@example.com" });
} else {
reject(new Error("Nieprawidłowe ID użytkownika"));
}
}, 500);
});
}
async function wyswietlDaneLubBlad(id) {
console.log(`Próba pobrania danych dla ID: ${id}`);
try {
const uzytkownik = await pobierzDaneUzytkownikaPromise(id);
console.log("--- Dane Użytkownika (async/await) ---");
console.log(`ID: ${uzytkownik.id}`);
console.log(`Imię: ${uzytkownik.imie}`);
console.log(`Email: ${uzytkownik.email}`);
console.log("-------------------------------------");
} catch (blad) {
console.error(`Błąd podczas pobierania danych dla ID ${id}: ${blad.message}`);
}
}
// Testowanie
wyswietlDaneLubBlad(789); // Powinien wyświetlić dane
wyswietlDaneLubBlad(-10); // Powinien wyświetlić błąd
Zadanie do samodzielnego wykonania
Przepisz rozwiązanie zadania do samodzielnego wykonania z poprzedniej lekcji (łańcuchowanie pomnozPrzezDwa
i dodajPiec
) używając składni async/await
i bloku try...catch
do obsługi ewentualnych błędów.
FAQ - Asynchroniczny JavaScript - Async/Await
Czy `async/await` blokuje główny wątek?
Nie. Chociaż składnia wygląda synchronicznie, await
w rzeczywistości wstrzymuje tylko wykonanie bieżącej funkcji async
, pozwalając pętli zdarzeń (event loop) na obsługę innych zadań. Główny wątek nie jest blokowany.
Czy mogę użyć `await` poza funkcją `async`?
Standardowo nie. await
jest dozwolone tylko bezpośrednio wewnątrz funkcji oznaczonych jako async
. Istnieje jednak eksperymentalna funkcja "top-level await", która pozwala używać await
na najwyższym poziomie modułów ES, ale jej wsparcie może się różnić.
Jak obsłużyć wiele operacji `await` równolegle?
Jeśli masz kilka operacji asynchronicznych, które nie zależą od siebie i mogą być wykonane równolegle, użycie await
jedna po drugiej byłoby nieefektywne. W takim przypadku lepiej użyć Promise.all()
w połączeniu z await
: const [wynikA, wynikB] = await Promise.all([operacjaA(), operacjaB()]);
.
Czy `async/await` zastępuje Promises?
Nie, async/await
jest zbudowane na bazie Promises. Funkcje async
zwracają Promises, a await
oczekuje na rozwiązanie Promises. Jest to raczej wygodniejszy sposób pracy z obietnicami niż ich zastępstwo.
Co jeśli zapomnę `await` przed funkcją zwracającą Promise?
Jeśli wywołasz funkcję zwracającą Promise bez await
wewnątrz funkcji async
, operacja asynchroniczna rozpocznie się, ale kod będzie kontynuowany natychmiast, nie czekając na jej zakończenie. Zmienna, do której przypiszesz wynik, będzie zawierać obiekt Promise (w stanie pending), a nie rozwiązany wynik.
Czy `try...catch` w `async` funkcji łapie wszystkie błędy?
try...catch
wewnątrz funkcji async
złapie błędy synchroniczne rzucone w bloku try
oraz błędy wynikające z odrzuconych obietnic, na które oczekiwano za pomocą await
. Nie złapie błędów z obietnic, na które nie użyto await
, ani błędów z kodu poza blokiem try
.