Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Tworzenie i obsługa obiektów Promise

W poprzedniej lekcji przyjrzeliśmy się funkcjom callback i problemowi "callback hell", który może wystąpić przy złożonych operacjach asynchronicznych. Teraz poznamy nowocześniejsze rozwiązanie - obiekty Promise (obietnice), które zapewniają elegantszy sposób obsługi asynchroniczności w JavaScript.

Czym są obiekty Promise?

Promise to obiekt reprezentujący ostateczne zakończenie (lub niepowodzenie) operacji asynchronicznej i jej rezultat. Można o nim myśleć jak o "obietnicy" uzyskania pewnego wyniku w przyszłości.

W kontekście Parku Jurajskiego, Promise jest jak zlecenie badania DNA dinozaura do laboratorium. Laboratoria nie zwracają wyników natychmiast - zamiast tego dają ci bilet z numerem, który reprezentuje "obietnicę" dostarczenia wyników, gdy badanie zostanie zakończone.

Promise może znajdować się w jednym z trzech stanów:

  • Pending (oczekiwanie) - stan początkowy, ani zakończony sukcesem, ani odrzucony
  • Fulfilled (spełniony) - operacja zakończyła się sukcesem, Promise został rozwiązany z wartością
  • Rejected (odrzucony) - operacja nie powiodła się, Promise został odrzucony z powodem (błędem)

Tworzenie Promise

Utworzenie nowego Promise w JavaScript wygląda następująco:

1const dnaAnalysisPromise = new Promise((resolve, reject) => {
2  // Symulacja badania DNA dinozaura (trwa 3 sekundy)
3  console.log("Analizowanie próbki DNA dinozaura...");
4
5  setTimeout(() => {
6    const successRate = Math.random();
7
8    if (successRate > 0.2) {
9      // Analiza zakończona sukcesem
10      const results = {
11        species: "Velociraptor",
12        age: "Około 68 milionów lat",
13        diet: "Mięsożerca",
14        geneticMarkers: ["ACGT-237", "TGGC-118", "GACT-339"]
15      };
16
17      resolve(results); // Spełnienie obietnicy
18    } else {
19      // Analiza zakończona niepowodzeniem
20      reject(new Error("Próbka DNA uszkodzona lub niewystarczająca")); // Odrzucenie obietnicy
21    }
22  }, 3000);
23});

W powyższym przykładzie:

  1. Tworzymy nowy obiekt Promise, który przyjmuje funkcję executor z dwoma argumentami:
    resolve
    i
    reject
  2. Wewnątrz funkcji executor wykonujemy operację asynchroniczną (symulowaną przez
    setTimeout
    )
  3. Jeśli operacja się powiedzie, wywołujemy
    resolve(wynik)
    aby spełnić obietnicę
  4. Jeśli operacja się nie powiedzie, wywołujemy
    reject(powód)
    aby odrzucić obietnicę

Użycie Promise - then, catch i finally

Aby obsłużyć wynik Promise, używamy metod

then()
,
catch()
i
finally()
:

1console.log("Wysłanie próbki do laboratorium...");
2
3dnaAnalysisPromise
4  .then(results => {
5    console.log("Analiza DNA zakończona sukcesem!");
6    console.log(`Gatunek: ${results.species}`);
7    console.log(`Wiek: ${results.age}`);
8    console.log(`Dieta: ${results.diet}`);
9    console.log("Markery genetyczne:", results.geneticMarkers.join(", "));
10    return results; // Możemy zwrócić wartość, która będzie przekazana do następnego .then()
11  })
12  .catch(error => {
13    console.error("Błąd analizy DNA:", error.message);
14    return "Brak danych"; // Zwracamy domyślną wartość w przypadku błędu
15  })
16  .finally(() => {
17    console.log("Procedura analizy DNA zakończona - niezależnie od wyniku.");
18    // finally nie przyjmuje żadnych argumentów i nic nie zwraca
19  });
20
21console.log("Kontynuacja pracy podczas analizy DNA...");

Wynik wykonania (w przypadku sukcesu):

1Wysłanie próbki do laboratorium...
2Analizowanie próbki DNA dinozaura...
3Kontynuacja pracy podczas analizy DNA...
4(po 3 sekundach)
5Analiza DNA zakończona sukcesem!
6Gatunek: Velociraptor
7Wiek: Około 68 milionów lat
8Dieta: Mięsożerca
9Markery genetyczne: ACGT-237, TGGC-118, GACT-339
10Procedura analizy DNA zakończona - niezależnie od wyniku.
  • then() - przyjmuje funkcję callback, która zostanie wywołana, gdy Promise zostanie spełniony (resolved). Callback otrzymuje wartość, z którą Promise został rozwiązany.
  • catch() - przyjmuje funkcję callback, która zostanie wywołana, gdy Promise zostanie odrzucony (rejected). Callback otrzymuje błąd, z którym Promise został odrzucony.
  • finally() - przyjmuje funkcję callback, która zostanie wywołana, gdy Promise zostanie zakończony, niezależnie od tego, czy został spełniony, czy odrzucony. Callback nie otrzymuje żadnych argumentów.

Łańcuchowanie obietnic (promise chaining)

Jedną z największych zalet Promise jest możliwość łańcuchowania operacji asynchronicznych. Każde wywołanie

.then()
zwraca nowy Promise, co pozwala na tworzenie sekwencji operacji asynchronicznych bez zagnieżdżania.

Wyobraźmy sobie proces tworzenia nowego dinozaura w Parku Jurajskim:

1function pobierzDNAZBazyDanych(gatunekId) {
2  return new Promise((resolve, reject) => {
3    console.log(`Pobieranie DNA dla gatunku ID: ${gatunekId}...`);
4    setTimeout(() => {
5      if (gatunekId > 0) {
6        resolve(`dna_sequence_${gatunekId}`);
7      } else {
8        reject(new Error("Nieprawidłowe ID gatunku"));
9      }
10    }, 1000);
11  });
12}
13
14function przygotujEmbrion(dnaSequence) {
15  return new Promise((resolve, reject) => {
16    console.log(`Przygotowywanie embriona z sekwencji DNA: ${dnaSequence}...`);
17    setTimeout(() => {
18      if (dnaSequence.includes("dna_sequence")) {
19        resolve(`embryo_${dnaSequence}`);
20      } else {
21        reject(new Error("Nieprawidłowa sekwencja DNA"));
22      }
23    }, 1500);
24  });
25}
26
27function inkubujJajo(embryo) {
28  return new Promise((resolve, reject) => {
29    console.log(`Inkubacja jaja z embrionem: ${embryo}...`);
30    setTimeout(() => {
31      if (embryo.includes("embryo_")) {
32        resolve(`dino_baby_${embryo.replace("embryo_", "")}`);
33      } else {
34        reject(new Error("Nieprawidłowy embrion"));
35      }
36    }, 2000);
37  });
38}
39
40function wychowajDinozaura(baby) {
41  return new Promise((resolve, reject) => {
42    console.log(`Wychowywanie młodego dinozaura: ${baby}...`);
43    setTimeout(() => {
44      if (baby.includes("dino_baby_")) {
45        resolve(`adult_dino_${baby.replace("dino_baby_", "")}`);
46      } else {
47        reject(new Error("Nieprawidłowy młody dinozaur"));
48      }
49    }, 1500);
50  });
51}
52
53// Użycie łańcucha obietnic
54console.log("Rozpoczynanie procesu tworzenia dinozaura...");
55
56pobierzDNAZBazyDanych(5)
57  .then(dna => {
58    console.log(`DNA pobrane: ${dna}`);
59    return przygotujEmbrion(dna); // Zwrócenie nowej obietnicy
60  })
61  .then(embryo => {
62    console.log(`Embrion przygotowany: ${embryo}`);
63    return inkubujJajo(embryo); // Zwrócenie nowej obietnicy
64  })
65  .then(baby => {
66    console.log(`Dinozaur wykluty: ${baby}`);
67    return wychowajDinozaura(baby); // Zwrócenie nowej obietnicy
68  })
69  .then(adultDino => {
70    console.log(`Dinozaur gotowy do wprowadzenia do parku: ${adultDino}`);
71  })
72  .catch(error => {
73    console.error("Błąd w procesie tworzenia dinozaura:", error.message);
74  })
75  .finally(() => {
76    console.log("Proces tworzenia dinozaura zakończony.");
77  });

Porównajmy to z wersją wykorzystującą callbacki, która wyglądałaby mniej więcej tak:

1pobierzDNAZBazyDanych(5, function(error, dna) {
2  if (error) {
3    console.error("Błąd pobierania DNA:", error.message);
4    console.log("Proces tworzenia dinozaura zakończony.");
5    return;
6  }
7
8  console.log(`DNA pobrane: ${dna}`);
9  przygotujEmbrion(dna, function(error, embryo) {
10    if (error) {
11      console.error("Błąd przygotowania embriona:", error.message);
12      console.log("Proces tworzenia dinozaura zakończony.");
13      return;
14    }
15
16    console.log(`Embrion przygotowany: ${embryo}`);
17    inkubujJajo(embryo, function(error, baby) {
18      if (error) {
19        console.error("Błąd inkubacji jaja:", error.message);
20        console.log("Proces tworzenia dinozaura zakończony.");
21        return;
22      }
23
24      console.log(`Dinozaur wykluty: ${baby}`);
25      wychowajDinozaura(baby, function(error, adultDino) {
26        if (error) {
27          console.error("Błąd wychowywania dinozaura:", error.message);
28          console.log("Proces tworzenia dinozaura zakończony.");
29          return;
30        }
31
32        console.log(`Dinozaur gotowy do wprowadzenia do parku: ${adultDino}`);
33        console.log("Proces tworzenia dinozaura zakończony.");
34      });
35    });
36  });
37});

Jak widać, wersja z Promise jest znacznie czytelniejsza i łatwiejsza w utrzymaniu.

Przekształcanie funkcji opartych na callbackach na Promise

Często musimy pracować z API lub bibliotekami, które używają funkcji opartych na callbackach. Możemy je przekształcić na Promise za pomocą funkcji opakowującej:

1// Funkcja oparta na callbacku (Node-style)
2function pobierzDaneWybiegu(wybiegId, callback) {
3  setTimeout(() => {
4    if (wybiegId <= 0) {
5      callback(new Error("Nieprawidłowe ID wybiegu"));
6      return;
7    }
8
9    const wybiegData = {
10      id: wybiegId,
11      nazwa: `Wybieg ${wybiegId}`,
12      powierzchnia: 1000 * wybiegId,
13      typ: wybiegId % 2 === 0 ? "Lądowy" : "Wodny"
14    };
15
16    callback(null, wybiegData);
17  }, 1000);
18}
19
20// Przekształcenie na Promise
21function pobierzDaneWybieguPromise(wybiegId) {
22  return new Promise((resolve, reject) => {
23    pobierzDaneWybiegu(wybiegId, (error, data) => {
24      if (error) {
25        reject(error); // Odrzucenie Promise w przypadku błędu
26      } else {
27        resolve(data); // Rozwiązanie Promise z danymi
28      }
29    });
30  });
31}
32
33// Użycie wersji z Promise
34pobierzDaneWybieguPromise(3)
35  .then(data => {
36    console.log("Dane wybiegu:", data);
37  })
38  .catch(error => {
39    console.error("Błąd:", error.message);
40  });

Ten wzorzec jest tak powszechny, że Node.js oferuje wbudowaną funkcję

util.promisify()
do automatycznego przekształcania funkcji opartych na callbackach na funkcje zwracające Promise.

Statyczne metody Promise

JavaScript oferuje kilka statycznych metod na obiekcie Promise, które są bardzo przydatne w różnych scenariuszach:

Promise.resolve() i Promise.reject()

Te metody tworzą Promise, który jest natychmiast rozwiązany lub odrzucony z podaną wartością:

1// Natychmiastowe rozwiązanie
2const resolvedPromise = Promise.resolve("Dane dinozaura");
3
4resolvedPromise.then(data => {
5  console.log("Rozwiązane z:", data); // "Rozwiązane z: Dane dinozaura"
6});
7
8// Natychmiastowe odrzucenie
9const rejectedPromise = Promise.reject(new Error("Brak dostępu do dinozaura"));
10
11rejectedPromise.catch(error => {
12  console.error("Odrzucone z:", error.message); // "Odrzucone z: Brak dostępu do dinozaura"
13});

Promise.all()

Przyjmuje tablicę obietnic i zwraca nową obietnicę, która rozwiązuje się, gdy wszystkie obietnice w tablicy zostaną rozwiązane, lub odrzuca się, gdy którakolwiek z obietnic zostanie odrzucona:

1// Pobieranie danych o trzech dinozaurach równolegle
2const dino1Promise = pobierzDaneDinozaura(1);
3const dino2Promise = pobierzDaneDinozaura(2);
4const dino3Promise = pobierzDaneDinozaura(3);
5
6Promise.all([dino1Promise, dino2Promise, dino3Promise])
7  .then(results => {
8    // results to tablica wyników w kolejności obietnic
9    console.log("Wszystkie dane dinozaurów pobrane:");
10    console.log("Dinozaur 1:", results[0]);
11    console.log("Dinozaur 2:", results[1]);
12    console.log("Dinozaur 3:", results[2]);
13
14    // Możemy teraz zrobić coś z wszystkimi danymi naraz
15    const łącznaWaga = results.reduce((sum, dino) => sum + dino.waga, 0);
16    console.log(`Łączna waga wszystkich dinozaurów: ${łącznaWaga} kg`);
17  })
18  .catch(error => {
19    // Jeśli choć jedna obietnica zostanie odrzucona, cała operacja się nie powiedzie
20    console.error("Nie udało się pobrać danych wszystkich dinozaurów:", error.message);
21  });

Promise.race()

Przyjmuje tablicę obietnic i zwraca nową obietnicę, która rozwiązuje się lub odrzuca z wynikiem pierwszej rozwiązanej lub odrzuconej obietnicy:

1// Symulacja monitorowania różnych systemów parku
2function monitorujOgrodzenie() {
3  return new Promise((resolve, reject) => {
4    setTimeout(() => {
5      if (Math.random() > 0.8) {
6        reject(new Error("Awaria ogrodzenia w sektorze B!"));
7      } else {
8        resolve("Ogrodzenie działa prawidłowo");
9      }
10    }, 1000 + Math.random() * 4000); // 1-5 sekund
11  });
12}
13
14function monitorujZasilanie() {
15  return new Promise((resolve, reject) => {
16    setTimeout(() => {
17      if (Math.random() > 0.9) {
18        reject(new Error("Awaria głównego zasilania!"));
19      } else {
20        resolve("Zasilanie działa prawidłowo");
21      }
22    }, 1000 + Math.random() * 4000); // 1-5 sekund
23  });
24}
25
26function monitorujKamery() {
27  return new Promise((resolve, reject) => {
28    setTimeout(() => {
29      if (Math.random() > 0.7) {
30        reject(new Error("Utrata sygnału z kamer w sektorze D!"));
31      } else {
32        resolve("Kamery działają prawidłowo");
33      }
34    }, 1000 + Math.random() * 4000); // 1-5 sekund
35  });
36}
37
38// Chcemy znać pierwszy alarm, który się pojawi
39Promise.race([
40  monitorujOgrodzenie(),
41  monitorujZasilanie(),
42  monitorujKamery()
43])
44  .then(status => {
45    console.log("Pierwszy status systemu:", status);
46  })
47  .catch(error => {
48    console.error("ALARM! Pierwszy wykryty problem:", error.message);
49    // Tutaj uruchamiamy procedury awaryjne
50    uruchomProceduręAwaryjną();
51  });
52
53function uruchomProceduręAwaryjną() {
54  console.log("Uruchamianie procedury awaryjnej...");
55  // Kod procedury awaryjnej
56}

Promise.allSettled()

Wprowadzony w ES2020, przyjmuje tablicę obietnic i zwraca nową obietnicę, która rozwiązuje się, gdy wszystkie obietnice w tablicy zostaną zakończone (rozwiązane lub odrzucone):

1// Monitorowanie wszystkich systemów parku
2Promise.allSettled([
3  monitorujOgrodzenie(),
4  monitorujZasilanie(),
5  monitorujKamery()
6])
7  .then(results => {
8    console.log("Status wszystkich systemów:");
9
10    // Każdy wynik ma status 'fulfilled' lub 'rejected'
11    results.forEach((result, index) => {
12      const systemNames = ["Ogrodzenie", "Zasilanie", "Kamery"];
13
14      if (result.status === 'fulfilled') {
15        console.log(`${systemNames[index]}: OK - ${result.value}`);
16      } else {
17        console.log(`${systemNames[index]}: BŁĄD - ${result.reason.message}`);
18      }
19    });
20
21    // Sprawdzenie, czy jakikolwiek system zgłosił alarm
22    const awarie = results.filter(result => result.status === 'rejected');
23    if (awarie.length > 0) {
24      console.log(`UWAGA: Wykryto ${awarie.length} awarie systemów!`);
25      uruchomProceduręAwaryjną();
26    }
27  });

Obsługa błędów w Promise

Prawidłowa obsługa błędów jest kluczowa, zwłaszcza w asynchronicznym kodzie. Promise zapewniają ujednolicony sposób obsługi błędów za pomocą metody

catch()
:

1pobierzDNAZBazyDanych(-1) // Nieprawidłowe ID, spowoduje błąd
2  .then(dna => {
3    console.log(`DNA pobrane: ${dna}`);
4    return przygotujEmbrion(dna);
5  })
6  .then(embryo => {
7    console.log(`Embrion przygotowany: ${embryo}`);
8    return inkubujJajo(embryo);
9  })
10  .then(baby => {
11    console.log(`Dinozaur wykluty: ${baby}`);
12    return wychowajDinozaura(baby);
13  })
14  .then(adultDino => {
15    console.log(`Dinozaur gotowy do wprowadzenia do parku: ${adultDino}`);
16  })
17  .catch(error => {
18    // Ten catch obsłuży błędy ze wszystkich poprzednich Promise
19    console.error("Błąd w procesie tworzenia dinozaura:", error.message);
20
21    // Możemy tu rejestrować błędy, informować użytkownika lub podejmować działania naprawcze
22    powiadomLabDoBadania();
23
24    // Możemy też zwrócić wartość, która zostanie przekazana do następnego .then()
25    return "Próba utworzenia dinozaura nie powiodła się";
26  })
27  .then(message => {
28    // Ten .then() zostanie wykonany po .catch()
29    console.log(message); // "Próba utworzenia dinozaura nie powiodła się"
30
31    // Możemy tu zainicjować procedury czyszczenia lub podjąć alternatywne działania
32    return ponowUruchomProcedurę();
33  })
34  .catch(error => {
35    // Ten catch obsłuży błędy z ponowUruchomProcedurę()
36    console.error("Błąd ponownego uruchomienia:", error.message);
37  });
38
39function powiadomLabDoBadania() {
40  console.log("Powiadamianie laboratorium o problemie z DNA...");
41}
42
43function ponowUruchomProcedurę() {
44  console.log("Ponowne uruchamianie procedury z zapasowym DNA...");
45  return "Procedura uruchomiona ponownie";
46}

Ważne zasady obsługi błędów w Promise:

  1. Metoda
    catch()
    przechwytuje błędy z wszystkich poprzedzających ją metod
    then()
  2. Jeśli w funkcji wewnątrz
    .then()
    zostanie wyrzucony wyjątek, Promise zostanie odrzucony z tym wyjątkiem
  3. Możesz mieć wiele metod
    catch()
    w łańcuchu Promise, aby obsłużyć różne typy błędów
  4. Po
    catch()
    można kontynuować łańcuch z kolejnymi metodami
    then()

Powszechne pułapki związane z Promise

1. Zapominanie o zwracaniu Promise

Częstym błędem jest zapominanie o zwracaniu Promise z funkcji, co prowadzi do nieoczekiwanego zachowania:

1// Błędnie - nie zwracamy Promise z funkcji
2function pobierzIZapiszDane(id) {
3  pobierzDaneDinozaura(id) // To zwraca Promise, ale nie przekazujemy go dalej
4    .then(data => {
5      return zapiszDane(data); // Ten Promise nie jest widoczny na zewnątrz
6    });
7
8  // Funkcja nie zwraca jawnie wartości, więc domyślnie zwraca undefined
9}
10
11// Poprawnie - zwracamy Promise
12function pobierzIZapiszDane(id) {
13  return pobierzDaneDinozaura(id) // Zwracamy Promise
14    .then(data => {
15      return zapiszDane(data); // Ten Promise jest teraz częścią łańcucha
16    });
17}

2. Nieobsługiwanie błędów

Nieobsłużone odrzucone Promise mogą powodować trudne do zdiagnozowania problemy:

1// Źle - brak obsługi błędów
2pobierzDaneDinozaura(999) // ID nie istnieje
3  .then(data => {
4    // Ten kod nie zostanie wykonany, gdy Promise zostanie odrzucony
5    wyświetlDane(data);
6  });
7// Jeśli Promise zostanie odrzucony, błąd nie zostanie obsłużony!
8
9// Dobrze - zawsze obsługuj błędy
10pobierzDaneDinozaura(999)
11  .then(data => {
12    wyświetlDane(data);
13  })
14  .catch(error => {
15    console.error("Nie udało się pobrać danych:", error.message);
16    wyświetlBłąd(error);
17  });

3. Promise Hell

Choć Promise pomagają uniknąć Callback Hell, można wpaść w podobną pułapkę, zagnieżdżając Promise zamiast ich łańcuchowania:

1// Źle - zagnieżdżanie Promise (Promise Hell)
2pobierzDaneDinozaura(1)
3  .then(dino => {
4    pobierzDaneWybiegu(dino.wybiegId) // Zagnieżdżony Promise
5      .then(wybieg => {
6        pobierzPracowników(wybieg.id) // Kolejny zagnieżdżony Promise
7          .then(pracownicy => {
8            console.log("Dane dinozaura:", dino);
9            console.log("Dane wybiegu:", wybieg);
10            console.log("Pracownicy:", pracownicy);
11          });
12      });
13  });
14
15// Dobrze - łańcuchowanie Promise
16pobierzDaneDinozaura(1)
17  .then(dino => {
18    dinoData = dino; // Zachowaj dane do późniejszego użycia
19    return pobierzDaneWybiegu(dino.wybiegId); // Zwróć Promise
20  })
21  .then(wybieg => {
22    wybiegData = wybieg; // Zachowaj dane do późniejszego użycia
23    return pobierzPracowników(wybieg.id); // Zwróć Promise
24  })
25  .then(pracownicy => {
26    console.log("Dane dinozaura:", dinoData);
27    console.log("Dane wybiegu:", wybiegData);
28    console.log("Pracownicy:", pracownicy);
29  })
30  .catch(error => {
31    console.error("Wystąpił błąd:", error.message);
32  });

Podsumowanie

Promise to potężny mechanizm JavaScript do obsługi operacji asynchronicznych, który oferuje wiele zalet w porównaniu z tradycyjnymi callbackami:

  1. Lepsza czytelność - Łańcuchowanie zamiast zagnieżdżania
  2. Lepsze zarządzanie błędami - Jednolity mechanizm obsługi błędów za pomocą
    .catch()
  3. Bardziej przewidywalny przepływ - Łatwiejsze sekwencjonowanie operacji asynchronicznych
  4. Łatwiejsze operacje równoległe - Metody takie jak
    Promise.all()
    i
    Promise.race()

Promise stanowią podstawę dla jeszcze nowszego mechanizmu asynchronicznego w JavaScript -

async/await
, który poznamy w przyszłych lekcjach. W Parku Jurajskim, jak i w prawdziwym programowaniu, dobre zarządzanie operacjami asynchronicznymi jest kluczem do tworzenia niezawodnych i łatwych w utrzymaniu systemów.

W kolejnym ćwiczeniu zobaczymy, jak łączyć Promise i tworzyć bardziej złożone przepływy asynchroniczne.

Przejdź do CodeWorlds