Menu

async/await w JavaScript: asynchroniczny kod czytany z góry na dół

Jak naprawdę działa async/await w JavaScript: funkcje async, czekanie na obietnice, obsługa błędów przez try/catch i równoległe uruchamianie zadań.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

async/await to po prostu przebrane obietnice

async/await nie jest nowym modelem współbieżności. To lukier składniowy na obietnicach (promise), który pozwala pisać kod, który wygląda na sekwencyjny, choć jest asynchroniczny. Ten sam mechanizm, znacznie przyjaźniejsza forma.

Oto to samo zadanie zapisane na oba sposoby:

Obie funkcje zwracają obietnicę. Obie robią dokładnie to samo. Wersję async czyta się z góry na dół, bez łańcucha .then, i właśnie na tym polega jej urok.

async oznacza, że funkcja zwraca obietnicę

Postaw async przed function, funkcją strzałkową albo metodą, a wydarzą się dwie rzeczy:

  1. Funkcja zawsze zwraca obietnicę. Cokolwiek zwrócisz przez return, staje się rozwiązaną wartością.
  2. Możesz używać w niej await.

Zwróć uwagę, że result nie jest stringiem, tylko obietnicą, która rozwiązuje się do stringa. Choć greet nie zawiera żadnego await ani pracy asynchronicznej, słowo kluczowe async i tak opakowuje zwracaną wartość w obietnicę. Jeśli funkcja rzuci wyjątek, obietnica zostanie odrzucona.

await wstrzymuje, dopóki obietnica się nie rozstrzygnie

Wewnątrz funkcji async wyrażenie await somePromise wstrzymuje tę funkcję do czasu rozwiązania obietnicy, a potem oddaje ci rozwiązaną wartość. Jeśli obietnica zostanie odrzucona, await rzuca wyjątek.

Obserwuj kolejność wyjścia. "odliczanie rozpoczęte" pojawia się przed "2", bo await wstrzymuje tylko funkcję async, a nie resztę programu. Event loop działa dalej, a countdown po prostu wznawia pracę później, gdy każda obietnica wait się rozwiąże.

Możesz robić await na wszystkim, co przypomina obietnicę. await 42 też jest poprawne: wartości niebędące obietnicami zostają opakowane w Promise.resolve(42) i rozwiązane od razu.

Obsługa błędów przez try/catch

Przy zwykłych obietnicach dołączasz .catch(). Przy async/await odrzucona obietnica staje się rzuconym wyjątkiem, który łapiesz w normalny sposób:

Jeden try/catch obejmuje każdy await w środku. Awarie sieci, błędy parsowania JSON i twoje własne instrukcje throw trafiają do tego samego catch. To realna poprawa w porównaniu z zagnieżdżonymi łańcuchami .then/.catch.

Jedna rzecz, na którą trzeba uważać: fetch odrzuca obietnicę tylko przy błędach sieci, a nie przy HTTP 4xx/5xx. Sam sprawdzasz res.ok i rzucasz błąd: ten wzorzec zobaczysz w prawdziwym kodzie na każdym kroku.

Nie rób await w pętli, jeśli nie musisz

To najczęstsza pułapka async/await. Sekwencyjne await w pętli oznacza, że każda iteracja czeka na poprzednią:

sequential trwa około 900 ms. parallel trwa około 300 ms. Zasada praktyczna: jeśli zadania nie zależą od swoich wyników, uruchom je wszystkie, a potem zrób await Promise.all. Rób await jedno po drugim tylko wtedy, gdy kolejne wywołanie naprawdę potrzebuje poprzedniego wyniku.

Dla kolekcji idiomem jest Promise.all(items.map(async (x) => ...)). Zwykłe for...of z await w środku działa szeregowo: czasem właśnie tego chcesz (ograniczanie liczby zapytań, zachowanie kolejności), ale zwykle nie.

Łączenie async/await i zwykłych obietnic

Nie musisz wybierać strony. Funkcje async zwracają obietnice, a await działa na każdej obietnicy, więc możesz je swobodnie mieszać:

Oba style są wymienne. Używaj await, gdy kod lepiej czyta się z góry na dół, a .then, gdy chcesz szybkiego jednorazowego wywołania albo pracujesz poza kontekstem async.

Top-level await (w modułach ES)

Dawniej trzeba było opakowywać await w funkcję async, bo await nie był dozwolony na najwyższym poziomie skryptu. To się zmieniło: wewnątrz modułu ES (pliku .mjs albo <script type="module">) możesz teraz robić await bezpośrednio na najwyższym poziomie:

// in an ES module
const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
const user = await res.json();
console.log(user.name);

Top-level await opóźnia zakończenie ładowania modułu, dopóki obietnica się nie rozstrzygnie, a każdy moduł, który go importuje, też czeka. Przydaje się do wczytywania konfiguracji i dynamicznych importów, ale używaj go oszczędnie: wolny top-level await blokuje wszystkich, którzy importują ten moduł.

W plikach CommonJS i zwykłych skryptach inline to nadal kończy się błędem SyntaxError. Klasyczne obejście to natychmiast wywoływana funkcja async:

Na czym ludzie się potykają

Szybki przegląd typowych pułapek:

  • Zapomniane async. Użycie await w zwykłej funkcji to błąd składni. Rozwiązanie: dodaj async albo wywołaj pomocniczą funkcję async przez .then.
  • Brak await na wyniku. const data = getJSON(url); daje ci obietnicę, a nie dane. Jeśli użyjesz jej jak wartości, na wyjściu zobaczysz [object Promise].
  • Nieobsłużone odrzucenia. Funkcja async wywołana bez czekania na wynik (doWork();) po cichu połknie błędy, chyba że dodasz .catch albo zrobisz na niej await wewnątrz try/catch.
  • forEach z callbackami async. array.forEach(async (x) => await something(x)) na nic nie czeka: forEach ignoruje zwracane obietnice. Użyj for...of z await albo Promise.all(array.map(...)).

Uruchom to: "koniec?" pojawia się przed jakimkolwiek "gotowe", bo zepsuta kończy działanie bez czekania. naprawiona czeka na wszystko i wypisuje "koniec!" na samym końcu.

Kiedy sięgać po async/await

Domyślnie używaj async/await w każdym kodzie, który wykonuje po kolei więcej niż jeden krok asynchroniczny albo potrzebuje obsługi błędów w stylu try/catch. Przy zwykłych obietnicach zostań w przypadku trywialnych jednolinijkowców, kodu bibliotek, który zwraca obietnicę bez potrzeby czekania na cokolwiek, albo gdy naprawdę potrzebujesz kombinatorów takich jak Promise.race czy .finally() w łańcuchu.

Dobrze użyte async/await sprawia, że kod asynchroniczny czyta się jak przepis: zrób to, potem to, potem to. Event loop nadal robi swoje, a ty po prostu przestajesz myśleć callbackami.

Dalej: fetch API

Większość przykładów na tej stronie używała fetch jako zastępstwa dla "jakiejś operacji asynchronicznej". Warto przyjrzeć mu się porządnie: jak działają żądania i odpowiedzi, obsługa JSON, ustawianie nagłówków i dlaczego fetch nie odrzuca obietnicy przy błędach HTTP. To temat następnej strony.

Najczęściej zadawane pytania

Co robi async/await w JavaScript?

async/await to składnia do pracy z obietnicami (promise), która pozwala pisać kod asynchroniczny tak, jakby był synchroniczny. async oznacza, że funkcja zwraca promise, a await wstrzymuje tę funkcję, dopóki podana obietnica się nie rozstrzygnie, a potem oddaje ci rozwiązaną wartość. Pod spodem to nadal obietnice, tylko łatwiejsze do czytania.

Czy można użyć await poza funkcją async?

Na najwyższym poziomie modułu ES tak: to tak zwany top-level await. W zwykłych funkcjach i skryptach CommonJS nie: await poza funkcją async to błąd składni. Rozwiązanie to zwykle opakowanie kodu w funkcję async i jej wywołanie albo przerobienie pliku na moduł ES.

Jak obsługiwać błędy przy async/await?

Owiń wywołania z await w try/catch. Każda odrzucona obietnica, na którą robisz await, zamienia się w rzucony wyjątek, który może obsłużyć blok catch. Do zadań w tle, na które nie robisz await, dołącz .catch() do zwróconej obietnicy, żeby odrzucenia nie zostały nieobsłużone.

Czy await blokuje cały program?

Nie. await wstrzymuje tylko bieżącą funkcję async. Event loop działa dalej: timery się odpalają, inne zadania asynchroniczne postępują, interfejs pozostaje responsywny. Kod wywołujący dostaje z powrotem oczekującą obietnicę i od razu idzie dalej.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ