Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds

Łączenie Promise (then, catch, finally)

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.

Przypomnienie metod Promise

Zanim przejdziemy do zaawansowanych technik, przypomnijmy podstawowe metody obiektu Promise:

  1. then(onFulfilled, onRejected) - rejestruje funkcje zwrotne do wykonania, gdy Promise zostanie spełniony lub odrzucony
  2. catch(onRejected) - rejestruje funkcję zwrotną do wykonania, gdy Promise zostanie odrzucony
  3. finally(onFinally) - rejestruje funkcję zwrotną do wykonania, gdy Promise zostanie zakończony (niezależnie od wyniku)

Łańcuchowanie Promise z then()

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:

  1. Każda funkcja zwraca Promise, który rozwiązuje się z danymi
  2. Każdy
    .then()
    przejmuje wynik z poprzedniego etapu i przekazuje go do następnej funkcji
  3. Dane są przekazywane przez cały łańcuch, z każdym etapem dodającym nowe informacje
  4. Pojedynczy
    .catch()
    na końcu przechwytuje błędy z każdego etapu
  5. .finally()
    jest wykonywane niezależnie od sukcesu lub porażki

Wartości zwracane z then() i catch()

Wartość 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  });

Obsługa błędów w łańcuchu Promise

Istnieje kilka strategii obsługi błędów w łańcuchach Promise:

1. Pojedynczy catch na końcu łańcucha

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  });

2. Wiele catch'ów dla różnych etapów

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}

3. Obsługa błędów wewnątrz funkcji then()

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}

Transformacja błędów

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  });

Używanie metody finally()

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  });

Zagnieżdżone Promise vs. łańcuchowanie

Ważne jest, aby unikać zagnieżdżania Promise i zamiast tego używać łańcuchowania. Porównajmy dwa podejścia:

Nieprawidłowe - zagnieżdżanie Promise

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  });

Prawidłowe - łańcuchowanie Promise

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  });

Złożone operacje z Promise

Sekwencyjne wykonanie operacji asynchronicznych na tablicy

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  });

Wykonanie operacji asynchronicznych z ograniczoną liczbą równoległych zadań

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  });

Dobre praktyki przy łączeniu Promise

  1. 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})
  2. 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  });
  3. 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  });
  4. 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}

Podsumowanie

Łą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:

  1. Zawsze zwracaj Promise z funkcji w
    .then()
    aby kontynuować łańcuch
  2. Używaj
    .catch()
    do jednolitej obsługi błędów
  3. Wykorzystuj
    .finally()
    do operacji czyszczących
  4. Unikaj zagnieżdżania Promise na rzecz łańcuchowania
  5. Rozbijaj skomplikowane operacje na mniejsze, nazwane funkcje

W następnym ćwiczeniu omówimy bardziej zaawansowane metody pracy z wieloma Promise jednocześnie, takie jak

Promise.all()
,
Promise.race()
i
Promise.allSettled()
.

Ir a CodeWorlds