W poprzednim ćwiczeniu poznaliśmy podstawy tworzenia i używania obiektów Promise. Teraz zagłębimy się w techniki łączenia obietnic, aby budować bardziej złożone przepływy asynchroniczne. W systemie tak skomplikowanym jak Park Jurajski, często musimy wykonywać sekwencje operacji asynchronicznych, obsługiwać błędy i zapewniać poprawne działanie systemów.
Zanim przejdziemy do zaawansowanych technik, przypomnijmy podstawowe metody obiektu Promise:
Metoda
.then() zawsze zwraca nowy obiekt Promise, co pozwala na budowanie łańcuchów operacji asynchronicznych. Każdy kolejny .then() otrzymuje wynik z poprzedniego.Wyobraźmy sobie scenariusz transportu nowego dinozaura do Parku Jurajskiego:
1function przygotujTransport(dinoId) {
2 console.log(`Przygotowywanie transportu dla dinozaura #${dinoId}...`);
3 return new Promise((resolve) => {
4 setTimeout(() => {
5 const transportInfo = {
6 id: dinoId,
7 vehicle: "Pojazd terenowy z zabezpieczoną klatką",
8 readyTime: new Date().toISOString()
9 };
10 console.log("Transport przygotowany!");
11 resolve(transportInfo);
12 }, 2000);
13 });
14}
15
16function załadujDinozaura(transportInfo) {
17 console.log(`Ładowanie dinozaura #${transportInfo.id} do ${transportInfo.vehicle}...`);
18 return new Promise((resolve, reject) => {
19 setTimeout(() => {
20 if (transportInfo.id > 5) {
21 // Większe ID = większy dinozaur = problemy z załadunkiem
22 reject(new Error("Dinozaur za duży dla standardowego transportu!"));
23 return;
24 }
25
26 const shipmentInfo = {
27 ...transportInfo,
28 loadedTime: new Date().toISOString(),
29 status: "Dinozaur bezpiecznie załadowany"
30 };
31 console.log("Dinozaur załadowany!");
32 resolve(shipmentInfo);
33 }, 3000);
34 });
35}
36
37function transportujDoParku(shipmentInfo) {
38 console.log(`Transportowanie dinozaura #${shipmentInfo.id} do parku...`);
39 return new Promise((resolve) => {
40 setTimeout(() => {
41 const deliveryInfo = {
42 ...shipmentInfo,
43 deliveryTime: new Date().toISOString(),
44 location: "Wybieg główny",
45 status: "Dostarczono"
46 };
47 console.log("Dinozaur bezpiecznie dostarczony do parku!");
48 resolve(deliveryInfo);
49 }, 4000);
50 });
51}
52
53function umieśćWWybiegu(deliveryInfo) {
54 console.log(`Przenoszenie dinozaura #${deliveryInfo.id} do wybiegu...`);
55 return new Promise((resolve, reject) => {
56 setTimeout(() => {
57 // Symulacja 20% szansy na niepowodzenie
58 if (Math.random() < 0.2) {
59 reject(new Error("Dinozaur wykazuje agresję podczas wprowadzania do wybiegu!"));
60 return;
61 }
62
63 const enclosureInfo = {
64 ...deliveryInfo,
65 enclosureId: `WYB-${deliveryInfo.id}`,
66 enclosureTime: new Date().toISOString(),
67 status: "W wybiegu"
68 };
69 console.log("Dinozaur bezpiecznie umieszczony w wybiegu!");
70 resolve(enclosureInfo);
71 }, 3000);
72 });
73}
74
75// Używamy łańcucha Promise
76console.log("Rozpoczynanie procesu transportu dinozaura #3...");
77
78przygotujTransport(3)
79 .then(transportInfo => {
80 console.log("Etap 1 zakończony:", transportInfo);
81 return załadujDinozaura(transportInfo);
82 })
83 .then(shipmentInfo => {
84 console.log("Etap 2 zakończony:", shipmentInfo);
85 return transportujDoParku(shipmentInfo);
86 })
87 .then(deliveryInfo => {
88 console.log("Etap 3 zakończony:", deliveryInfo);
89 return umieśćWWybiegu(deliveryInfo);
90 })
91 .then(enclosureInfo => {
92 console.log("Proces zakończony sukcesem!");
93 console.log("Raport końcowy:", enclosureInfo);
94 })
95 .catch(error => {
96 console.error("Wystąpił błąd podczas procesu:", error.message);
97 console.log("Uruchamianie procedur awaryjnych...");
98 })
99 .finally(() => {
100 console.log("Proces transportu zakończony - raport został wysłany do administracji.");
101 });
102
103console.log("Inne operacje mogą być wykonywane równolegle...");W powyższym przykładzie:
.then() przejmuje wynik z poprzedniego etapu i przekazuje go do następnej funkcji.catch() na końcu przechwytuje błędy z każdego etapu.finally() jest wykonywane niezależnie od sukcesu lub porażkiWartość zwracana z funkcji callback w
.then() lub .catch() staje się wartością, z którą rozwiązuje się Promise zwracany przez tę metodę. Daje nam to możliwość transformacji danych na każdym etapie łańcucha.1// Przykład transformacji danych w łańcuchu Promise
2pobierzDaneDinozaura(5)
3 .then(dinoData => {
4 // Przekształcenie danych dinozaura na format używany w UI
5 return {
6 id: dinoData.id,
7 name: dinoData.name || "Nieznany",
8 display: `${dinoData.species} (${dinoData.age} lat)`,
9 dangerLevel: dinoData.carnivore ? "Wysoki" : "Niski",
10 location: dinoData.enclosureId
11 };
12 })
13 .then(uiData => {
14 // Dodajemy dodatkowe informacje UI
15 return {
16 ...uiData,
17 icon: uiData.dangerLevel === "Wysoki" ? "🔴" : "🟢",
18 description: `${uiData.icon} ${uiData.display} - Zagrożenie: ${uiData.dangerLevel}`
19 };
20 })
21 .then(finalData => {
22 console.log("Dane dla UI:", finalData);
23 updateDinoCard(finalData); // Aktualizacja interfejsu użytkownika
24 });Istnieje kilka strategii obsługi błędów w łańcuchach Promise:
1przygotujTransport(7) // Dinozaur z ID 7 jest za duży dla standardowego transportu
2 .then(transportInfo => załadujDinozaura(transportInfo))
3 .then(shipmentInfo => transportujDoParku(shipmentInfo))
4 .then(deliveryInfo => umieśćWWybiegu(deliveryInfo))
5 .then(enclosureInfo => {
6 console.log("Sukces:", enclosureInfo);
7 })
8 .catch(error => {
9 // Ten catch obsłuży błędy ze wszystkich poprzednich kroków
10 console.error("Wystąpił błąd:", error.message);
11 notyfikujAdministrację(error);
12 });1przygotujTransport(7)
2 .then(transportInfo => załadujDinozaura(transportInfo))
3 .catch(error => {
4 // Ten catch obsługuje błędy tylko z przygotujTransport i załadujDinozaura
5 console.error("Błąd podczas przygotowania lub załadunku:", error.message);
6 return zamówPowiększonyTransport(7); // Alternatywna ścieżka - zwracamy nowy Promise
7 })
8 .then(info => {
9 // Ten then otrzyma albo wynik załadujDinozaura, albo zamówPowiększonyTransport
10 return transportujDoParku(info);
11 })
12 .then(deliveryInfo => umieśćWWybiegu(deliveryInfo))
13 .catch(error => {
14 // Ten catch obsługuje błędy z transportujDoParku, umieśćWWybiegu lub zamówPowiększonyTransport
15 console.error("Błąd podczas transportu lub umieszczania w wybiegu:", error.message);
16 notyfikujZespółBezpieczeństwa(error);
17 throw error; // Przekazujemy błąd dalej
18 })
19 .catch(error => {
20 // Ten catch przechwytuje wszystkie nieobsłużone błędy
21 console.error("Krytyczny błąd w procesie:", error.message);
22 aktywujProceduryAwaryjne();
23 });
24
25function zamówPowiększonyTransport(dinoId) {
26 console.log(`Zamawianie powiększonego transportu dla dinozaura #${dinoId}...`);
27 // Implementacja zwracająca Promise
28 return Promise.resolve({
29 id: dinoId,
30 vehicle: "Wzmocniony transport specjalny XL",
31 status: "Przygotowany"
32 });
33}1przygotujTransport(3)
2 .then(transportInfo => {
3 try {
4 // Kod, który może wyrzucić wyjątki
5 walidujTransport(transportInfo);
6 return załadujDinozaura(transportInfo);
7 } catch (error) {
8 // Obsługa synchronicznych wyjątków
9 console.error("Błąd walidacji transportu:", error.message);
10 return naprawTransport(transportInfo); // Zwracamy nowy Promise
11 }
12 })
13 .then(shipmentInfo => transportujDoParku(shipmentInfo))
14 .catch(error => {
15 // Obsługa pozostałych błędów
16 console.error("Błąd procesu:", error.message);
17 });
18
19function walidujTransport(transportInfo) {
20 if (!transportInfo.vehicle.includes("klatka")) {
21 throw new Error("Transport nie ma odpowiedniego zabezpieczenia!");
22 }
23}
24
25function naprawTransport(transportInfo) {
26 console.log("Naprawianie problemów z transportem...");
27 return Promise.resolve({
28 ...transportInfo,
29 vehicle: transportInfo.vehicle + " z wzmocnioną klatką",
30 status: "Naprawiony"
31 });
32}Metoda
.catch() może nie tylko obsłużyć błąd, ale także przetransformować go w sukces, zwracając wartość:1pobierzDaneDinozaura(999) // ID nie istnieje
2 .catch(error => {
3 console.log("Nie znaleziono dinozaura:", error.message);
4 // Zamiast propagować błąd, zwracamy dane domyślne
5 return {
6 id: 999,
7 name: "Nieznany",
8 species: "Nieznany gatunek",
9 age: "?",
10 carnivore: true,
11 enclosureId: "N/A"
12 };
13 })
14 .then(dinoData => {
15 // Ten then zostanie wykonany z danymi domyślnymi
16 console.log("Dane dinozaura (domyślne lub rzeczywiste):", dinoData);
17 updateUI(dinoData);
18 });Metoda
.finally() jest używana do wykonania kodu czyszczącego, niezależnie od tego, czy Promise został spełniony czy odrzucony. Jest to idealne miejsce dla kodu, który musi zostać wykonany w obu przypadkach:1let connectionToDatabase = null;
2
3connectToDinosaurDatabase()
4 .then(connection => {
5 connectionToDatabase = connection;
6 return connection.query("SELECT * FROM dinosaurs WHERE species = 'Velociraptor'");
7 })
8 .then(raptors => {
9 console.log(`Znaleziono ${raptors.length} velociraptorów w bazie danych`);
10 processRaptors(raptors);
11 })
12 .catch(error => {
13 console.error("Błąd podczas pracy z bazą danych:", error.message);
14 notifyDatabaseAdmin(error);
15 })
16 .finally(() => {
17 // Ten kod wykonuje się zawsze, niezależnie od sukcesu lub porażki
18 if (connectionToDatabase) {
19 console.log("Zamykanie połączenia z bazą danych...");
20 connectionToDatabase.close(); // Zawsze zamykamy połączenie
21 }
22 });Ważne jest, aby unikać zagnieżdżania Promise i zamiast tego używać łańcuchowania. Porównajmy dwa podejścia:
1// Złe podejście - zagnieżdżanie Promise prowadzi do "Promise Hell"
2pobierzDaneDinozaura(1)
3 .then(dino => {
4 console.log("Dinozaur:", dino.name);
5
6 pobierzDaneWybiegu(dino.enclosureId) // Zagnieżdżony Promise
7 .then(enclosure => {
8 console.log("Wybieg:", enclosure.name);
9
10 pobierzPracowników(enclosure.id) // Kolejny zagnieżdżony Promise
11 .then(staff => {
12 console.log("Pracownicy:", staff.map(s => s.name).join(", "));
13
14 // Jeszcze głębsze zagnieżdżenie...
15 pobierzHistorięKarmienia(dino.id)
16 .then(feedingHistory => {
17 console.log("Historia karmienia:", feedingHistory);
18 })
19 .catch(error => {
20 console.error("Błąd historii karmienia:", error.message);
21 });
22 })
23 .catch(error => {
24 console.error("Błąd pracowników:", error.message);
25 });
26 })
27 .catch(error => {
28 console.error("Błąd wybiegu:", error.message);
29 });
30 })
31 .catch(error => {
32 console.error("Błąd dinozaura:", error.message);
33 });1// Dobre podejście - łańcuchowanie Promise
2// Używamy zmiennych dla danych, które będziemy potrzebować w dalszych krokach
3let dinoData, enclosureData;
4
5pobierzDaneDinozaura(1)
6 .then(dino => {
7 dinoData = dino; // Zapisujemy dane dinozaura
8 console.log("Dinozaur:", dino.name);
9 return pobierzDaneWybiegu(dino.enclosureId); // Zwracamy Promise
10 })
11 .then(enclosure => {
12 enclosureData = enclosure; // Zapisujemy dane wybiegu
13 console.log("Wybieg:", enclosure.name);
14 return pobierzPracowników(enclosure.id); // Zwracamy Promise
15 })
16 .then(staff => {
17 console.log("Pracownicy:", staff.map(s => s.name).join(", "));
18 return pobierzHistorięKarmienia(dinoData.id); // Zwracamy Promise
19 })
20 .then(feedingHistory => {
21 console.log("Historia karmienia:", feedingHistory);
22 // Możemy korzystać z zapisanych wcześniej danych
23 console.log(`Podsumowanie dla ${dinoData.name} w wybiegu ${enclosureData.name}`);
24 })
25 .catch(error => {
26 // Jeden catch obsługuje wszystkie błędy
27 console.error("Wystąpił błąd:", error.message);
28 });Czasami potrzebujemy wykonać operację asynchroniczną na każdym elemencie tablicy, jeden po drugim:
1// Sekwencyjne karmienie dinozaurów
2const dinosaurIds = [1, 2, 3, 4, 5];
3
4// Funkcja karmienia jednego dinozaura
5function nakarmDinozaura(id) {
6 return new Promise((resolve, reject) => {
7 console.log(`Rozpoczynam karmienie dinozaura #${id}...`);
8 setTimeout(() => {
9 if (id === 3) {
10 reject(new Error(`Dinozaur #${id} odmawia jedzenia!`));
11 return;
12 }
13 console.log(`Dinozaur #${id} nakarmiony!`);
14 resolve({id, status: 'nakarmiony', time: new Date().toISOString()});
15 }, 1000);
16 });
17}
18
19// Sekwencyjne karmienie wszystkich dinozaurów
20dinosaurIds.reduce((promise, dinoId) => {
21 return promise
22 .then(results => {
23 // results to tablica wyników z poprzednich operacji
24 return nakarmDinozaura(dinoId)
25 .then(result => {
26 // Dodaj wynik do tablicy wyników
27 return [...results, result];
28 })
29 .catch(error => {
30 console.error(`Błąd karmienia dinozaura #${dinoId}:`, error.message);
31 // Dodaj informację o błędzie do wyników
32 return [...results, {id: dinoId, status: 'błąd', error: error.message}];
33 });
34 });
35}, Promise.resolve([])) // Rozpocznij z pustą tablicą wyników
36 .then(allResults => {
37 console.log("Wszystkie dinozaury obsłużone:");
38 console.log(allResults);
39
40 // Sprawdzamy, czy wszystkie dinozaury zostały nakarmione
41 const allFed = allResults.every(result => result.status === 'nakarmiony');
42 if (allFed) {
43 console.log("Wszystkie dinozaury zostały nakarmione!");
44 } else {
45 console.log("Nie wszystkie dinozaury zostały nakarmione - sprawdź raport.");
46 }
47 });1// Funkcja do wykonania operacji asynchronicznych z ograniczoną liczbą równoległych zadań
2async function wykonajZOgraniczeniem(tablica, funkcjaPromise, limitRównolegle) {
3 const wszystkieWyniki = [];
4
5 // Wykonujemy zadania w porcjach
6 for (let i = 0; i < tablica.length; i += limitRównolegle) {
7 const porcja = tablica.slice(i, i + limitRównolegle);
8
9 // Uruchamiamy Promise.all dla porcji zadań
10 const wyniki = await Promise.all(
11 porcja.map(element =>
12 funkcjaPromise(element)
13 .catch(error => {
14 console.error(`Błąd dla elementu ${element}:`, error.message);
15 return { element, status: 'błąd', error: error.message };
16 })
17 )
18 );
19
20 wszystkieWyniki.push(...wyniki);
21 console.log(`Zakończono porcję ${i / limitRównolegle + 1} (${wyniki.length} operacji)`);
22 }
23
24 return wszystkieWyniki;
25}
26
27// Użycie funkcji do równoległego, ale ograniczonego sprawdzania zdrowia dinozaurów
28const wszystkieIdDinozaurów = Array.from({ length: 20 }, (_, i) => i + 1);
29
30function sprawdźZdrowieDinozaura(id) {
31 return new Promise((resolve, reject) => {
32 console.log(`Sprawdzam zdrowie dinozaura #${id}...`);
33 setTimeout(() => {
34 // Symulacja losowego stanu zdrowia
35 const health = Math.floor(Math.random() * 100);
36 if (health < 30) {
37 reject(new Error(`Dinozaur #${id} ma problemy zdrowotne! Poziom zdrowia: ${health}`));
38 return;
39 }
40 console.log(`Dinozaur #${id} jest zdrowy. Poziom zdrowia: ${health}`);
41 resolve({id, health, status: 'zdrowy'});
42 }, 500 + Math.random() * 1000); // Losowy czas wykonania
43 });
44}
45
46// Sprawdzamy zdrowie maksymalnie 5 dinozaurów naraz
47wykonajZOgraniczeniem(wszystkieIdDinozaurów, sprawdźZdrowieDinozaura, 5)
48 .then(wyniki => {
49 console.log("Sprawdzanie zdrowia wszystkich dinozaurów zakończone:");
50
51 const zdrowe = wyniki.filter(w => w.status === 'zdrowy').length;
52 const chore = wyniki.filter(w => w.status === 'błąd').length;
53
54 console.log(`Zdrowe: ${zdrowe}, Wymagające uwagi: ${chore}`);
55
56 // Wyświetl dinozaury wymagające uwagi
57 if (chore > 0) {
58 console.log("Dinozaury wymagające interwencji weterynaryjnej:");
59 wyniki
60 .filter(w => w.status === 'błąd')
61 .forEach(w => console.log(`- Dinozaur #${w.element}: ${w.error}`));
62 }
63 });Zawsze zwracaj Promise z funkcji then() - zapewnia to prawidłowe łańcuchowanie i propagację wyników:
1.then(data => {
2 // Źle - nie zwraca Promise, łańcuch zostanie przerwany
3 processData(data);
4})
5
6.then(data => {
7 // Dobrze - zwraca wynik przetwarzania do następnego then()
8 return processData(data);
9})Używaj jednego catch() dla grupy powiązanych operacji - dzięki temu kod jest bardziej czytelny:
1pobierzDaneWybiegu(5)
2 .then(data => updateEnclosureStatus(data))
3 .then(status => notifyStaff(status))
4 .catch(error => {
5 // Jeden catch dla całej operacji aktualizacji wybiegu
6 handleEnclosureError(error);
7 });
8
9// Nowa, niepowiązana operacja z własnym catch
10pobierzListeGości()
11 .then(visitors => updateVisitorCount(visitors))
12 .catch(error => {
13 // Oddzielny catch dla operacji związanej z gośćmi
14 handleVisitorError(error);
15 });Używaj finally() do kodu czyszczącego - niezależnie od sukcesu czy porażki:
1otwórzBrameWybiegu(enclosureId)
2 .then(() => przeniośDinozaura(dinoId, enclosureId))
3 .catch(error => {
4 logError("Błąd przenoszenia:", error);
5 throw error; // Propaguj błąd dalej
6 })
7 .finally(() => {
8 // Zawsze zamykaj bramę, niezależnie od wyniku
9 zamknijBrameWybiegu(enclosureId);
10 });Unikaj zbyt głębokich łańcuchów Promise - rozbijaj je na mniejsze, nazwane funkcje:
1// Zamiast jednego długiego łańcucha:
2function przenieśDinozauraDoNowegoWybiegu(dinoId, newEnclosureId) {
3 return pobierzDaneDinozaura(dinoId)
4 .then(dino => {
5 // Sprawdz aktualny wybieg
6 return pobierzDaneWybiegu(dino.enclosureId)
7 .then(oldEnclosure => {
8 // Przygotuj nowy wybieg
9 return pobierzDaneWybiegu(newEnclosureId)
10 .then(newEnclosure => {
11 // Sprawdz kompatybilność
12 // ... więcej zagnieżdżonych then...
13 });
14 });
15 });
16}
17
18// Lepiej - rozbij na mniejsze funkcje:
19function przenieśDinozauraDoNowegoWybiegu(dinoId, newEnclosureId) {
20 return pobierzDaneDinozauraZAktualnymWybiegiem(dinoId)
21 .then(data => sprawdźKompatybilnośćWybiegów(data, newEnclosureId))
22 .then(data => wykonajTransfer(data));
23}
24
25function pobierzDaneDinozauraZAktualnymWybiegiem(dinoId) {
26 let dinoData;
27 return pobierzDaneDinozaura(dinoId)
28 .then(dino => {
29 dinoData = dino;
30 return pobierzDaneWybiegu(dino.enclosureId);
31 })
32 .then(oldEnclosure => {
33 return { dino: dinoData, oldEnclosure };
34 });
35}
36
37function sprawdźKompatybilnośćWybiegów(data, newEnclosureId) {
38 return pobierzDaneWybiegu(newEnclosureId)
39 .then(newEnclosure => {
40 // Sprawdz kompatybilność
41 return { ...data, newEnclosure };
42 });
43}
44
45function wykonajTransfer(data) {
46 // Implementacja transferu
47 return Promise.resolve({ status: "zakończony", ...data });
48}Łączenie Promise za pomocą
.then(), .catch() i .finally() daje potężne narzędzia do budowania złożonych przepływów asynchronicznych. Dobrze zaprojektowany łańcuch Promise może sprawiać, że nawet skomplikowane operacje asynchroniczne, takie jak zarządzanie transportem dinozaurów w Parku Jurajskim, stają się czytelne i łatwe w utrzymaniu.Kluczowe zasady to:
.then() aby kontynuować łańcuch.catch() do jednolitej obsługi błędów.finally() do operacji czyszczącychW następnym ćwiczeniu omówimy bardziej zaawansowane metody pracy z wieloma Promise jednocześnie, takie jak
Promise.all(), Promise.race() i Promise.allSettled().