Lekcja 15: Asynchroniczny JavaScript - Obietnice (Promises)
W poprzedniej lekcji omówiliśmy funkcje zwrotne (callbacks) jako sposób radzenia sobie z asynchronicznością w JavaScript, ale zauważyliśmy też problem "Callback Hell". Aby rozwiązać ten problem i ułatwić zarządzanie operacjami asynchronicznymi, w ES6 wprowadzono Obietnice (Promises).
Co to jest Obietnica (Promise)?
Obietnica to obiekt reprezentujący ostateczny wynik (lub błąd) operacji asynchronicznej. Zamiast przekazywać callback do funkcji asynchronicznej, funkcja ta zwraca obiekt obietnicy. Do tego obiektu możemy następnie "podpiąć" funkcje, które mają zostać wykonane, gdy operacja się zakończy (sukcesem lub porażką).
Obietnica może znajdować się w jednym z trzech stanów:
- Pending (Oczekująca): Stan początkowy, operacja asynchroniczna jeszcze się nie zakończyła.
- Fulfilled (Spełniona/Rozwiązana): Operacja asynchroniczna zakończyła się sukcesem. Obietnica przechowuje wynik operacji.
- Rejected (Odrzucona): Operacja asynchroniczna zakończyła się błędem. Obietnica przechowuje powód błędu (zazwyczaj obiekt Error).
Obietnica jest obiektem "jednorazowym" - gdy przejdzie ze stanu pending do fulfilled lub rejected, jej stan i wynik (lub błąd) już się nie zmienią.
Tworzenie Obietnicy
Obietnice tworzymy za pomocą konstruktora new Promise()
. Konstruktor przyjmuje jako argument funkcję (tzw. "executor"), która sama otrzymuje dwa argumenty: funkcję resolve
i funkcję reject
.
- Wewnątrz funkcji "executor" wykonujemy operację asynchroniczną.
- Jeśli operacja zakończy się sukcesem, wywołujemy
resolve(wynik)
, przekazując wynik operacji. To zmienia stan obietnicy na fulfilled. - Jeśli operacja zakończy się błędem, wywołujemy
reject(błąd)
, przekazując powód błędu. To zmienia stan obietnicy na rejected.
function symulujPobieranieDanych(czySukces) {
// Zwracamy nową obietnicę
return new Promise((resolve, reject) => {
console.log("Rozpoczynam pobieranie danych (Promise)...");
setTimeout(() => {
if (czySukces) {
const dane = { id: 1, name: "Produkt A" };
console.log("Dane pobrane pomyślnie.");
// Operacja udana - rozwiązujemy obietnicę z wynikiem
resolve(dane);
} else {
const blad = new Error("Nie udało się pobrać danych!");
console.error("Wystąpił błąd podczas pobierania.");
// Operacja nieudana - odrzucamy obietnicę z błędem
reject(blad);
}
}, 1500); // Symulacja opóźnienia 1.5 sekundy
});
}
// Użycie funkcji zwracającej Promise
const obietnicaDanych = symulujPobieranieDanych(true);
console.log("Obietnica utworzona, stan: pending", obietnicaDanych);
// Co się stanie po 1.5 sekundy? Obietnica zmieni stan...
// Jak zareagować na zmianę stanu - patrz poniżej.
Konsumowanie Obietnicy (`.then()`, `.catch()`, `.finally()`)
Aby zareagować na zmianę stanu obietnicy, używamy metod dołączanych do obiektu Promise:
promise.then(onFulfilled, onRejected)
:onFulfilled
: Funkcja, która zostanie wywołana, gdy obietnica zostanie spełniona (fulfilled). Otrzymuje wynik przekazany doresolve()
jako argument.onRejected
(opcjonalny): Funkcja, która zostanie wywołana, gdy obietnica zostanie odrzucona (rejected). Otrzymuje błąd przekazany doreject()
jako argument.
.then()
sama zwraca nową obietnicę, co umożliwia łączenie operacji (chaining).promise.catch(onRejected)
: Skrót dlapromise.then(null, onRejected)
. Służy do obsługi błędów (stanu rejected). Również zwraca nową obietnicę.promise.finally(onFinally)
(ES2018): FunkcjaonFinally
zostanie wywołana niezależnie od tego, czy obietnica została spełniona, czy odrzucona. Przydatne do wykonywania operacji czyszczących (np. ukrywanie wskaźnika ładowania). Nie otrzymuje wyniku ani błędu jako argumentu. Zwraca nową obietnicę.
// Kontynuacja poprzedniego przykładu
// 1. Użycie .then() z dwoma argumentami
symulujPobieranieDanych(true)
.then(
(dane) => {
console.log("Sukces (.then - onFulfilled):", dane);
},
(blad) => {
console.error("Błąd (.then - onRejected):", blad.message);
}
);
// 2. Użycie .then() dla sukcesu i .catch() dla błędu (preferowane)
symulujPobieranieDanych(false) // Tym razem symulujemy błąd
.then((dane) => {
// Ten blok się nie wykona, bo obietnica zostanie odrzucona
console.log("Sukces (.then + .catch):", dane);
})
.catch((blad) => {
// Ten blok obsłuży błąd
console.error("Błąd (.catch):", blad.message);
});
// 3. Użycie .finally()
symulujPobieranieDanych(true)
.then((dane) => {
console.log("Dane z finally:", dane);
// Można tu zwrócić coś, co będzie wynikiem kolejnego .then()
return dane.name;
})
.catch((blad) => {
console.error("Błąd z finally:", blad.message);
// Można tu rzucić błąd dalej lub zwrócić wartość domyślną
throw blad; // Rzucenie błędu dalej
})
.finally(() => {
// Ten blok wykona się zawsze, po .then() lub .catch()
console.log("Operacja zakończona (finally) - czyszczenie zasobów...");
})
.then((nazwaProduktu) => {
// Ten .then() otrzyma wynik z poprzedniego .then() (jeśli nie było błędu)
console.log("Wynik po finally (jeśli sukces):", nazwaProduktu);
})
.catch((blad) => {
// Ten .catch() złapie błąd rzucony z poprzedniego .catch()
console.error("Błąd po finally (jeśli błąd):", blad.message);
});
Łańcuchowanie Obietnic (Promise Chaining)
Ponieważ .then()
i .catch()
zwracają nowe obietnice, możemy je łączyć w łańcuchy, aby wykonywać sekwencyjne operacje asynchroniczne w bardziej czytelny sposób niż przy użyciu zagnieżdżonych callbacków.
- Jeśli funkcja
onFulfilled
w.then()
zwróci wartość, ta wartość staje się wynikiem nowej obietnicy zwróconej przez.then()
. - Jeśli funkcja
onFulfilled
zwróci inną obietnicę, nowa obietnica zwrócona przez.then()
"przyjmie" stan i wynik tej zwróconej obietnicy. To pozwala czekać na zakończenie kolejnej operacji asynchronicznej. - Jeśli wystąpi błąd w którymkolwiek kroku łańcucha (lub obietnica zostanie odrzucona), sterowanie przechodzi do najbliższego bloku
.catch()
w dół łańcucha.
function krokA(wartosc) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Krok A zakończony");
resolve(wartosc + 1);
}, 500);
});
}
function krokB(wartosc) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("Krok B zakończony");
resolve(wartosc * 2);
}, 500);
});
}
function krokC(wartosc) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (wartosc > 20) {
console.log("Krok C zakończony");
resolve(wartosc - 3);
} else {
reject(new Error("Wartość w kroku C jest za mała!"));
}
}, 500);
});
}
console.log("Rozpoczynam łańcuch obietnic...");
krokA(10)
.then((wynikA) => {
console.log("Wynik kroku A:", wynikA); // 11
// Zwracamy obietnicę z kroku B
return krokB(wynikA);
})
.then((wynikB) => {
console.log("Wynik kroku B:", wynikB); // 22
// Zwracamy obietnicę z kroku C
return krokC(wynikB);
})
.then((wynikC) => {
console.log("Wynik kroku C:", wynikC); // 19
console.log("Wszystkie kroki zakończone pomyślnie!");
})
.catch((blad) => {
// Ten blok złapie błąd z dowolnego poprzedniego kroku
console.error("Wystąpił błąd w łańcuchu:", blad.message);
})
.finally(() => {
console.log("Koniec łańcucha (finally).");
});
console.log("Łańcuch obietnic zainicjowany.");
Łańcuchowanie obietnic jest znacznie bardziej czytelne niż "Callback Hell".
Zadanie praktyczne
Przepisz funkcję pobierzDaneUzytkownika(id, callback)
z poprzedniej lekcji (wersję z obsługą błędu), aby zamiast przyjmować callback, zwracała obiekt Promise. Obietnica powinna być rozwiązana (resolve) z obiektem użytkownika, jeśli ID jest poprawne, lub odrzucona (reject) z błędem, jeśli ID jest niepoprawne (<= 0).
Następnie użyj nowej funkcji, wywołując ją z poprawnym ID i użyj .then()
do wyświetlenia danych oraz .catch()
do obsłużenia ewentualnego błędu.
Pokaż rozwiązanie
function pobierzDaneUzytkownikaPromise(id) {
return new Promise((resolve, reject) => {
console.log(`Pobieranie danych (Promise) dla użytkownika o ID: ${id}...`);
setTimeout(() => {
if (id > 0) {
const daneUzytkownika = {
id: id,
imie: "Anna",
email: "anna@example.com"
};
console.log("Dane pobrane (Promise).");
resolve(daneUzytkownika);
} else {
const blad = new Error("Nieprawidłowe ID użytkownika (Promise)");
console.error("Błąd ID (Promise).");
reject(blad);
}
}, 1000);
});
}
// Wywołanie z poprawnym ID
pobierzDaneUzytkownikaPromise(456)
.then(uzytkownik => {
console.log("--- Dane Użytkownika (Promise) ---");
console.log(`ID: ${uzytkownik.id}`);
console.log(`Imię: ${uzytkownik.imie}`);
console.log(`Email: ${uzytkownik.email}`);
console.log("---------------------------------");
})
.catch(blad => {
console.error("Błąd pobierania (poprawne ID):", blad.message);
});
// Wywołanie z niepoprawnym ID
pobierzDaneUzytkownikaPromise(-5)
.then(uzytkownik => {
// Ten blok się nie wykona
console.log("Dane użytkownika (niepoprawne ID):", uzytkownik);
})
.catch(blad => {
console.error("Błąd pobierania (niepoprawne ID):", blad.message);
});
console.log("Zapytania o dane użytkownika (Promise) wysłane.");
Zadanie do samodzielnego wykonania
Stwórz dwie funkcje zwracające Promise:
pomnozPrzezDwa(liczba)
: Po 500ms rozwiązuje obietnicę z wynikiemliczba * 2
.dodajPiec(liczba)
: Po 500ms rozwiązuje obietnicę z wynikiemliczba + 5
.
Użyj łańcuchowania obietnic, aby wywołać pomnozPrzezDwa(10)
, a następnie na wyniku tej operacji wywołać dodajPiec
. Wyświetl końcowy wynik w konsoli za pomocą .then()
. Dodaj też blok .catch()
na końcu łańcucha do obsługi ewentualnych błędów.
FAQ - Asynchroniczny JavaScript - Promises
Czy mogę użyć `await` z Promise?
Tak! Składnia `async/await`, którą poznamy w następnej lekcji, jest zbudowana na bazie Promises i stanowi jeszcze wygodniejszy sposób pracy z nimi, pozwalając pisać kod asynchroniczny w sposób wyglądający bardziej jak kod synchroniczny.
Co jeśli zapomnę dodać `.catch()`?
Jeśli obietnica zostanie odrzucona, a nie ma żadnego bloku .catch()
(lub drugiego argumentu w .then()
) do obsłużenia tego błędu, spowoduje to tzw. "unhandled promise rejection". W większości środowisk JavaScript (przeglądarki, Node.js) spowoduje to wyświetlenie błędu w konsoli, a w niektórych przypadkach może nawet zakończyć działanie aplikacji (np. w Node.js).
Czy mogę rozwiązać (resolve) lub odrzucić (reject) obietnicę więcej niż raz?
Nie. Obietnica może zmienić stan z pending tylko raz - albo na fulfilled (po wywołaniu resolve
), albo na rejected (po wywołaniu reject
). Kolejne wywołania resolve
lub reject
dla tej samej obietnicy są ignorowane.
Co zwraca funkcja wewnątrz `.then()` lub `.catch()`?
Funkcja wewnątrz .then()
lub .catch()
może zwrócić zwykłą wartość lub inną obietnicę. Jeśli zwróci wartość, staje się ona wynikiem nowej obietnicy zwróconej przez .then()/.catch()
. Jeśli zwróci obietnicę, łańcuch poczeka na jej rozwiązanie.
Do czego służą `Promise.all()`, `Promise.race()`, `Promise.allSettled()`?
Są to statyczne metody obiektu `Promise` służące do pracy z wieloma obietnicami naraz. Promise.all()
czeka na spełnienie wszystkich obietnic (lub odrzucenie pierwszej). Promise.race()
czeka na spełnienie lub odrzucenie pierwszej obietnicy. Promise.allSettled()
czeka, aż wszystkie obietnice zakończą działanie (niezależnie od sukcesu czy porażki).
Czy Promises całkowicie eliminują callbacki?
Nie. Chociaż Promises rozwiązują problem "Callback Hell" dla sekwencyjnych operacji asynchronicznych, same metody .then()
, .catch()
i .finally()
przyjmują funkcje zwrotne jako argumenty. Callbacki są nadal fundamentalnym elementem JavaScript, np. w obsłudze zdarzeń.