Menu

Promise w JavaScript: then, catch i Promise.all

Jak działają Promise w JavaScript: trzy stany, łańcuchy z then i catch, łączenie przez Promise.all i tworzenie własnych przez new Promise.

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

Promise to miejsce na przyszłą wartość

Gdy JavaScript musi zrobić coś, co zajmuje czas (zapytanie sieciowe, odczyt pliku, czekanie na timer), nie może od razu podać ci wyniku. Zamiast tego daje ci Promise: obiekt reprezentujący wartość, która w końcu się pojawi.

Pierwszy console.log pokazuje Promise w stanie oczekiwania. Pół sekundy później Promise się rozwiązuje, a callback z .then uruchamia się z wartością. Sam Promise to zwykły obiekt. Cała magia polega na tym, że wie, jak powiadomić wszystkich nasłuchujących, gdy pojawi się jego wartość.

Trzy stany

Promise zawsze jest w jednym z trzech stanów:

  • pending: praca trwa. Wartości jeszcze nie ma.
  • fulfilled: praca się udała. Wartość jest dostępna.
  • rejected: praca się nie udała. Dostępny jest błąd.

Promise przechodzi ze stanu pending do fulfilled albo rejected dokładnie raz i zostaje w nim na zawsze. Nie da się cofnąć rozwiązania Promise ani rozwiązać go dwa razy.

Promise.resolve(value) tworzy Promise, który już jest spełniony, a Promise.reject(error) taki, który już jest odrzucony. Przydają się w testach i do zwracania Promise z funkcji, która czasem zna odpowiedź od razu.

Odczyt wartości: .then i .catch

Nie wyciągasz wartości z Promise bezpośrednio. Przekazujesz callback do .then, a Promise wywołuje go, gdy wartość jest gotowa:

.catch(fn) uruchamia się, gdy Promise zostanie odrzucony. Pod spodem to skrót od .then(undefined, fn). Jeden .catch() na końcu łańcucha obsługuje odrzucenia z każdego kroku powyżej, więc nie potrzebujesz go po każdym .then.

Łańcuchy: każde .then zwraca nowy Promise

To jest miejsce, w którym wiele osób się gubi. .then() nie tylko uruchamia callback. Zwraca nowy Promise, który rozwiązuje się tym, co zwrócił callback. Dzięki temu można tworzyć łańcuchy:

Każdy krok zasila następny. Jeśli callback w .then zwróci Promise, łańcuch czeka na ten Promise, zanim pójdzie dalej, więc kroki asynchroniczne ładnie się składają:

Trzy kolejne kroki asynchroniczne i żadnego zagnieżdżania. Porównaj to z tą samą logiką napisaną na callbackach, a zobaczysz, dlaczego Promise się przyjęły.

Błędy spływają w dół łańcucha

Odrzucony Promise pomija każde .then, dopóki nie trafi na .catch. Na tym polega cały model obsługi błędów:

Rzucenie wyjątku wewnątrz .then odrzuca Promise, który to .then zwróciło. Kolejne .then widzi odrzucenie i przekazuje je dalej, aż .catch je przechwyci. Zwykle wystarczy jeden .catch na końcu łańcucha, a łańcuch w ogóle bez .catch wygeneruje ostrzeżenie „unhandled promise rejection”, które warto naprawić.

Własny Promise przez new Promise

Najczęściej korzystasz z Promise, które dają ci biblioteki. Czasem jednak trzeba opakować coś, co Promise nie zwraca, zwykle stare API oparte na callbackach:

Funkcja przekazana do new Promise to tak zwany executor. Dostaje dwa argumenty: resolve (wywołaj z wartością sukcesu) i reject (wywołaj z błędem). Wywołaj jeden z nich dokładnie raz. Kolejne wywołania są ignorowane.

Dwa nawyki, które oszczędzają kłopotów:

  • Sięgaj po new Promise tylko wtedy, gdy opakowujesz coś, co nie jest jeszcze oparte na Promise. Jeśli funkcja już zwraca Promise, po prostu go zwróć.
  • Zawsze wywołuj reject z obiektem Error, a nie ze stringiem. Stack trace warto zachować.

Równoległe zadania: Promise.all

Łańcuchy .then działają sekwencyjnie. Gdy masz kilka niezależnych zadań asynchronicznych i chcesz, żeby działały jednocześnie, narzędziem jest Promise.all:

Wszystkie trzy timery działają równolegle. Promise.all rozwiązuje się tablicą wyników w tej samej kolejności co wejście, gdy tylko spełni się każdy Promise. Całkowity czas to około 400 ms, a nie 900 ms.

Haczyk: Promise.all zostaje odrzucony, gdy tylko którykolwiek z jego Promise zostanie odrzucony, a pozostałe wyniki przepadają. To właściwe zachowanie, gdy potrzebujesz wszystkich elementów (np. do wyrenderowania strony, która wymaga trzech wywołań API). Gdy nie potrzebujesz, użyj allSettled.

Gdy część błędów jest dopuszczalna: Promise.allSettled

Promise.allSettled czeka, aż każdy Promise się zakończy, spełniony albo odrzucony, i daje ci raport:

Każdy wynik to obiekt: { status: "fulfilled", value } albo { status: "rejected", reason }. Przydaje się, gdy częściowy sukces jest do przyjęcia: logowanie paczki zdarzeń, pobieranie wielu miniatur, uruchamianie niezależnych health checków.

Dwa inne kombinatory warte poznania:

  • Promise.race([...]): kończy się, gdy tylko zakończy się pierwszy Promise, sukcesem albo błędem. Przydatne przy timeoutach.
  • Promise.any([...]): spełnia się pierwszym sukcesem i ignoruje odrzucenia. Zostaje odrzucony tylko wtedy, gdy odrzucone zostaną wszystkie Promise.

Promise są zawsze asynchroniczne

Nawet Promise, który już jest rozwiązany, wywołuje callback z .then asynchronicznie, nigdy synchronicznie i nigdy w tym samym ticku:

Wynik to przed, po, natychmiast. Callback z .then czeka, aż bieżący kod się skończy, a potem uruchamia się z kolejki mikrozadań. Ta zasada („callback Promise nigdy nie uruchamia się synchronicznie”) sprawia, że łączenie Promise z kodem synchronicznym jest przewidywalne: kod synchroniczny zawsze kończy się pierwszy.

Dalej: async/await

Łańcuchy wywołań .then działają, ale przy więcej niż dwóch czy trzech krokach zaczynają przypominać schody. async/await to składnia zbudowana na Promise, która pozwala napisać tę samą logikę tak, jakby była synchroniczna, z try/catch do obsługi błędów i zwykłymi zmiennymi na wartości pośrednie. O tym dalej.

Najczęściej zadawane pytania

Czym jest Promise w JavaScript?

Promise to obiekt reprezentujący wartość, która jeszcze nie jest gotowa, zwykle przyszły wynik operacji asynchronicznej, na przykład zapytania sieciowego. Zawsze jest w jednym z trzech stanów: pending, fulfilled albo rejected. Docelową wartość odczytujesz, podpinając callbacki przez .then() i .catch().

Czym różni się then od catch?

.then(onFulfilled) uruchamia się, gdy Promise zakończy się sukcesem, i dostaje rozwiązaną wartość. .catch(onRejected) uruchamia się, gdy Promise (albo dowolny wcześniejszy Promise w łańcuchu) zostanie odrzucony, i dostaje błąd. Jeden .catch() na końcu łańcucha obsługuje błędy z każdego kroku powyżej.

Co robi Promise.all?

Promise.all([p1, p2, p3]) przyjmuje tablicę Promise i zwraca jeden Promise, który spełnia się tablicą wszystkich rozwiązanych wartości, ale dopiero wtedy, gdy spełnią się wszystkie wejściowe Promise. Jeśli którykolwiek zostanie odrzucony, całość od razu zostaje odrzucona. Użyj Promise.allSettled, gdy chcesz dostać każdy wynik bez względu na błędy.

Używać Promise czy async/await?

To ten sam mechanizm: async/await to składnia zbudowana na Promise. Większość nowego kodu czyta się lepiej z async/await, ale nadal zwracasz Promise, nadal łapiesz błędy przez try/catch albo .catch() i nadal sięgasz po Promise.all, żeby uruchamiać zadania równolegle. Znajomość Promise sprawia, że async/await staje się zrozumiałe.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ