Poznajemy promisy w JavaScript. Już się tym zajmowaliśmy, ale teraz zbiorczo podsumowujemy wszystko, co o nich wiemy. Do dzieła.

Ok, promise może mieć jeden z trzech stanów:

  • pending – promise zostało utworzone, ale ani resolve ani reject nie zostało jeszcze zawołane, kod nie dotarł do tego miejsca
  • fulfilled – zostało zawołane resolve z jakąś wartością
  • rejected – zostało zawołane reject z jakąś wartością

Mówi się też, konceptualnie, że mogą mieć dwa stany:

  • pending – promise zostało utworzone, ale ani resolve, ani reject nie zostało jeszcze zawołane
  • settled – zostało zawołane resolve lub reject, promise jest albo fulfilled albo rejected, nie jest pending

Ok, jak tworzymy promises:

  • Tworzymy obiekt promise i ta promise zostaje w tym momencie pending
  • Jeżeli chcemy mieć fabrykę promis, to potrzebujemy funkcję, która zwraca obiekt promise
  • Konstruktor promise przyjmuje jeden argument, callback
  • Callback przyjmuje, w tej kolejności, argumenty resolve i reject (konwencja nazewnicza dowolna)
  • Te argumenty służą zawołaniu ich jako funkcję z dowolną wartością i są odpowiednikiem funkcji Promise.resolve i Promise.reject
  • Teoretycznie nie musimy korzystać z tych argumentów – możemy statycznie zawołać sobie Promise.resolve albo Promise.reject

Ok, jakiś prosty przykład z MDN:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

promise1.then((value) => {
  console.log(value);
  // Expected output: "foo"
});

console.log(promise1);
// Expected output: [object Promise]

Czyli od czasu utworzenia new Promise do czasu zawołania resolve promise jest pending. Wtedy można jakiś inny kod wykonywać. Jak tylko promise zostanie resolved, wykona się pierwszy argument callbacka do then, którym jest onFulfilled callback, przyjmujący wartość.

Czym jest then:

  • Pierwszy then służy do obsługi resolve i opcjonalnie reject
  • Kolejne theny służą do chainowania return value z poprzednich thenów (pod warunkiem, że poprzednie zrobią returna)

Przykład z MDN:

const p1 = new Promise((resolve, reject) => {
  resolve("Success!");
  // or
  // reject(new Error("Error!"));
});

p1.then(
  (value) => {
    console.log(value); // Success!
  },
  (reason) => {
    console.error(reason); // Error!
  },
);

Jeżeli zrobimy resolve, wykona się pierwszy callback z then, jak już promise będzie settled. Jeżeli reject, wywoła się drugi callback do then, jak tylko promise będzie reject.

Można olać drugi callback i łapać przez catch te rzeczy, ale to nie jest dobry pomysł. Kilka uwag:

  • Jeżeli zrobimy catch przed then to catch złapie reject
  • Jeżeli zrobimy then z drugim callbackiem, ale drugi callback nie wyrzuci błędu, to catch nie wyłapie go
  • Jeżeli pierwszy callback then (ten obowiązkowy) lub drugi wywoła jakiś błąd, to catch go złapie

Czyli tak:

  • Do obsługi błędów, które mogą wywołać się podczas, gdy promise jest pending powinniśmy wewnątrz przekazanego do jej konstruktora callbacku używać normalnych metod obsługi error-handling (try-catch)
  • Do obsługi sytuacji, w której promise jest settled używamy pierwszy then
  • Do obsługi sytuacji, w której promise jest fulfilled mamy pierwszy callback w pierwszym then
  • Do obsługi sytuacji, w której promise jest rejected, i tylko tej sytuacji, mamy drugi callback w pierwszym then
  • Jeden i drugi przyjmuje wartość, która została do resolve/reject wrzucona
  • Do obsługi błędów, jakie mogą wystąpić w naszych callbackach w then, mamy metodę catch i używajmy jej tylko do tego
  • Jeżeli nasze callbacki z then coś zwracają, to można podpiąć do nich kolejny then, już z jednym callbackiem
  • Teoretycznie można w pierwszym then zawołać Promise.reject i w drugim then mieć dwa callbacki, teoretycznie…

Ok, promise.catch, głupi przykład (jak nie używać):

p1.then((value) => {
  console.log(value); // "Success!"
  return Promise.reject("oh, no!");
})
  .catch((e) => {
    console.error(e); // "oh, no!"
  })
  .then(
    () => console.log("after a catch the chain is restored"), // "after a catch the chain is restored"
    () => console.log("Not fired due to the catch"),
  );

Tutaj widzimy, w pierwszym then robi reject, i catch go łapie. Ten drugi then ma zupełnie niepotrzebny drugi callback (no chyba, że jeszcze raz byśmy wywołali reject) zaś pierwszy nie otrzyma żadnej wartości.

Tutaj robimy coś głupiego – używamy catch do łapania reject. Od tego jest drugi callback pierwszego then!

Ok, kolejny, też niezbyt mądry przykład:

const p1 = new Promise((resolve, reject) => {
  throw new Error("Uh-oh!");
});

p1.catch((e) => {
  console.error(e); // "Uh-oh!"
});

Tu używamy catch do łapania błędu, który powstał wewnątrz ciała callbacka promise, podczas gdy promise jest pending. Wewnątrz tego callbacka powinniśmy używać try/catch i odpowiednio reject lub resolve, do łapania resolve i reject mamy pierwszy then z dwoma callbackami, do łapania błędów w tych callbackach mamy catch!

Lepszy przykład:

const p1 = new Promise((resolve, reject) => {
  resolve("Success");
});

p1.then((value) => {
  console.log(value); // "Success!"
  throw new Error("oh, no!");
})
  .catch((e) => {
    console.error(e.message); // "oh, no!"
  })
  .then(
    () => console.log("after a catch the chain is restored"), // "after a catch the chain is restored"
    () => console.log("Not fired due to the catch"),
  );

To są z MDN przykłady, nie są to przykłady dobrego ani użytecznego kodu, to już mieliśmy, to są przykłady jak ten mechanizm działa. I tak, jeżeli callback w then nam jakiś błąd wywali, to możemy to przez catch łapać.

Dodajmy – kolejne theny się wywołają (aby drugi callback zadziałał im musielibyśmy Promise.reject wywołać), ale bez wartości, catch nie stopuje tych thenów. Zatem myślmy, jak układamy kod i pamiętajmy, że mamy try-catch, które możemy używać wszędzie.

Te metody klasy Promise i sama klasa są mylące (async await jest dużo lepsze i czytelniejsze), ale musimy to ogarniać pracując w JavaScript, choć teraz już te promisy nie są aż tak ważne.

Ok, metoda all – bierze promisy i zwraca jedną promise, gdy wszystkie promisy będą fulfilled:

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values);
});
// Expected output: Array [3, 42, "foo"]

Metoda allSettled – bierze promisy i zwraca jedną promise, gdy wszystkie promisy będą settled (rejected lub resolved):

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
  setTimeout(reject, 100, 'foo'),
);
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) =>
  results.forEach((result) => console.log(result.status)),
);

// Expected output:
// "fulfilled"
// "rejected"

Mamy też race, która bierze promisy, i odpala się, gdy napotka na pierwszą, której udało się zostać resolved:

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);
  // Both resolve, but promise2 is faster
});
// Expected output: "two"