Uwaga! Trwają prace nad nową wersją serwisu. Mogą występować przejściowe problemy z jego funkcjonowaniem. Przepraszam za niedogodności!

⛔ Potrzebujesz wsparcia? Oceny CV? A może Code Review? ✅ Dołącz do naszej społeczności na Discordzie!

Jak uzależnić wykonanie kodu asynchronicznego od statusu odpowiedzi z serwera

Zdobądź nowe umiejętności obsługi API

Pierwsze próby z API i kodem asynchronicznym za Tobą? Wiesz już, jak odebrać dane i przechwycić błąd? W takim razie czas na uzależnienie naszych akcji od statusu odpowiedzi (np. 200 czy 404), czyli informacji przychodzących z serwera. Zobacz, dlaczego może to być problematyczne i jak sobie z tym poradzić.

Spis treści

 Response.ok() – odpowiedź prawidłowa

Być może do tej pory przy obsłudze API korzystałeś z właściwości .ok interfejsu Response. Zawiera ona wartość logiczną określającą, czy odpowiedź z serwera zakończyła się powodzeniem (status odpowiedzi w zakresie 200-299), czy nie.

Jeśli odpowiedź zakończyła się powodzeniem, to zwracasz dane otrzymane z serwera i wykorzystujesz je dalej w kodzie. Przykładowo: pobieramy dane z API o kotach i wyświetlamy na stronie jako paragraf.

// Pobieramy fakt o kocie i wyświetlamy jako paragraf
const fact = get('fact').then(data => {
  const factEl = document.createElement('p');
  factEl.innerText = data.fact;
  document.body.appendChild(factEl);
});

function get(resource) {
  return _prepareFetch(resource);
}

function _prepareFetch(resource = '', options = { method: 'GET' }) {
  const path = 'https://catfact.ninja' + `/${resource}`;
  const promise = fetch(path, options);
  return promise
    .then(resp => {
      // Jeśli odpowiedź zakończyła się powodzeniem, to zwracamy dane otrzymane z serwera (tu: zamieniamy JSON na obiekt za pomocą metody .json())
      if (resp.ok) {
        return resp.json();
      }
      return Promise.reject(resp);
    })
    .catch(err => console.error(err))
    .finally(() => {
      console.log('Odpytywanie API zakończone!');
    });
}

Lecz co, jeżeli chciałbyś obsługiwać statusy błędów na wyższym poziomie – tak, by funkcja _prepareFetch() była uniwersalna? Chciałbyś np. wyświetlać inną wiadomość dla błędu 404 w różnych funkcjonalnościach. Jak widzisz, jest to numer spoza zakresu dla powodzenia odpowiedzi (200-299), więc nie możemy go obsłużyć w warunku:

//…
      if (resp.ok) {
        return resp.json();
      }
//…

Może by zwracać z funkcji _prepareFetch() obiekt, w którym zawrzemy zarówno status odpowiedzi, jak i dane z API? Zobaczmy.

 Czy obiekt ze statusem odpowiedzi i danymi z API zadziała

Teoretycznie moglibyśmy zwracać obiekt, który zawierałby status odpowiedzi oraz dane z serwera, np. typu JSON:

const fact = get('fact').then(data => {
  console.log(data.content); // sprawdź wartość `data.content` w konsoli
  const factEl = document.createElement('p');
  if (data.code >= 200 && data.code <= 299) {
    factEl.innerText = data.content.fact;
  } else if (data.code === 404) {
    factEl.innerText = 'Żądany zasób nie istnieje.';
  } else {
    factEl.innerText = 'Nie udało się wyświetlić faktu';
  }
  document.body.appendChild(factEl);
});

function get(resource) {
  return _prepareFetch(resource);
}

function _prepareFetch(resource = '', options = { method: 'GET' }) {
  const path = 'https://catfact.ninja' + `/${resource}`;
  const promise = fetch(path, options);
  return promise
    .then(resp => {
      return { content: resp.json(), code: resp.status };
    })
    .finally(() => {
      console.log('Odpytywanie API zakończone!');
    });
}

Niestety rozwiązanie to nie działa. Okazuje się, że choć obietnica została spełniona (status: fulfilled), to nie mamy dostępu do przesłanych danych (data.content), ponieważ status obietnicy to nadal pending (oczekujący). Dlaczego? Dzieje się tak, ponieważ metoda .json() zwraca obietnicę, zatem trzeba by dodatkowo obsłużyć wynik jej działania:

const fact = get('fact').then(data => {
  const factEl = document.createElement('p');
  if (data.code >= 200 && data.code <= 299) {
    data.content.then(content => (factEl.innerText = content.fact));
  } else if (data.code === 404) {
    factEl.innerText = 'Żądany zasób nie istnieje.';
  } else {
    factEl.innerText = 'Nie udało się wyświetlić faktu';
  }
  document.body.appendChild(factEl);
});

Zauważ, że kod zaczyna wyglądać nie najlepiej. Zagnieździliśmy w sobie obsługę obietnic. A co, jeśli do naszego obiektu ze statusem i JSON-em doszłaby jeszcze jedna obietnica? Jej działanie również trzeba by obsłużyć, co uczyniłoby kod jeszcze mniej czytelnym.

Jest jednak sposób, by poczekać na rozwiązanie wszystkich obietnic – metoda Promise.all().

Uwaga: w kodzie nie mamy już metody .catch(). Jeżeli korzystalibyśmy z niej w funkcji _prepareFetch(), to nasze rozwiązanie ze zwróceniem statusu odpowiedzi (np. 404) by nie zadziałało. Odpowiedzi ze statusem spoza zakresu 200-299 byłyby obsługiwane właśnie przez blok .catch().

 Metoda Promise.all() jako rozwiązanie problemu

Metoda Promise.all() przyjmuje tablicę obietnic jako dane wejściowe i zwraca pojedynczą obietnicę. Ta zwrócona obietnica spełnia się, gdy spełnią się wszystkie obietnice wejściowe.

Dzięki temu nie musimy już osobno obsługiwać obietnicy zwracanej przez Response.json() – odpowiedź z niej otrzymamy w tablicy zwróconej z Promise.all().

Zauważ, że poniżej do tablicy w argumencie metody .all() przekazujemy też resp.status, choć właściwość ta nie przechowuje obietnicy. Zostanie ona przez metodę .all() potraktowana jako obietnica spełniona.

Tym sposobem otrzymujemy tablicę, którą destrukturyzujemy zależnie od potrzeb w miejscu wykorzystania:

const fact = get('fact').then(([content, code]) => {
  const factEl = document.createElement('p');
  if (code >= 200 && code <= 299) {
    factEl.innerText = content.fact;
    document.body.appendChild(factEl);
  } else if (code === 404) {
    factEl.innerText = 'Żądany fakt nie istnieje.';
  } else {
    factEl.innerText = 'Nie udało się wyświetlić faktu';
  }
  document.body.appendChild(factEl);
});

function get(resource) {
  return _prepareFetch(resource);
}

function _prepareFetch(resource = '', options = { method: 'GET' }) {
  const path = 'https://catfact.ninja' + `/${resource}`;
  const promise = fetch(path, options);
  return promise
    .then(resp => {
      return Promise.all([resp.json(), resp.status]);
    })
    .finally(() => {
      console.log('Odpytywanie API zakończone!');
    });
}

To rozwiązanie również ma pewien minus – tablica nie powinna mieć zbyt wielu elementów, inaczej jej destrukturyzacja stanie się „upierdliwa” (trzeba bowiem zachować kolejność elementów w tablicy). Rozwiązuje to jednak problem zagnieżdżania obsługi obietnic.

 Obsługa wielu statusów i problem z długimi instrukcjami warunkowymi

Jeżeli będziemy chcieli obsłużyć więcej statusów, to pomnożymy liczbę ifów: jeśli status to zakres od 200 do 299 to…, jeśli status to 301, to…, jeśli status to 404 to…, jeśli status większy od 499 to…

Długie instrukcje nie tylko są słabo czytelne, ale też trudno je reużywać i modyfikować. Aby dowiedzieć się, jak radzić sobie z tym problemem, zapoznaj się z artykułem „Mniej instrukcji warunkowych”.

 Sprawdzanie działania kodu dla różnych statusów odpowiedzi

Gdy korzystamy z API, na działanie którego nie mamy wpływu (czyli np. nie jest to serwer lokalny stworzony za pomocą JSON Servera), a chcemy przetestować obsługę różnych statusów odpowiedzi, możemy podmienić nasz URL na https://httpstat.us z numerem statusu, który nas interesuje.

Przykład tego widzisz poniżej – sprawdzam tam odpowiedź ze statusem 500. Zwróć uwagę, że zgodnie z dokumentacją trzeba było dodać w opcjach nagłówek Accept z wartością application/json, aby otrzymać odpowiedź w formacie JSON.

const fact = get('fact').then(([content, code]) => {
  const factEl = document.createElement('p');
  if (code >= 200 && code <= 299) {
    factEl.innerText = content.fact;
    document.body.appendChild(factEl);
  } else if (code === 404) {
    factEl.innerText = 'Żądany fakt nie istnieje.';
  } else {
    factEl.innerText = 'Nie udało się wyświetlić faktu';
  }
  document.body.appendChild(factEl);
});

function get(resource) {
  return _prepareFetch(resource);
}

function _prepareFetch(
  resource = '',
  options = {
    method: 'GET',
    headers: {
      Accept: 'application/json',
    },
  }
) {
  const path = 'https://httpstat.us/500' + `/${resource}`;
  const promise = fetch(path, options);
  return promise
    .then(resp => {
      return Promise.all([resp.json(), resp.status]);
    })
    .finally(() => {
      console.log('Odpytywanie API zakończone!');
    });
}

 Metody Promise.race() i Promise.any()

Przy okazji metody .all() warto przypomnieć sobie działanie .race() i .any() – obie jako argument również przyjmują tablicę obietnic.

Promise.race() zwraca obietnicę ze stanem takim, jaki ma pierwsza („najszybsza”) rozwiązana obietnica – bez względu na to, czy została ona spełniona (fulfilled) czy odrzucona (rejected).

Promise.any() zwraca pierwszą spełnioną obietnicę. Jeśli wszystkie obietnice zostaną odrzucone, zwraca tablicę z powodami odrzucenia każdej z obietnic.

 

Dopiero zaczynasz przygodę z asynchronicznym JavaScriptem? Skorzystaj z JSON Servera i naucz się dodawać, edytować, usuwać i pobierać dane z lokalnej bazy danych. Zapraszam do artykułu „JSON Server – asynchroniczny JavaScript z lokalnym API”.

Udostępnij ten artykuł:

Mentoring to efektywna nauka pod okiem doświadczonej osoby, która:

  • przekazuje Ci swoją wiedzę i nadzoruje Twoje postępy w zdobywaniu umiejętności,
  • uczy Cię dobrych praktyk i wyłapuje złe nawyki,
  • wspiera Twój rozwój i zwiększa zaangażowanie w naukę.

Mam coś dla Ciebie!

W każdy piątek rozsyłam motywujący do nauki programowania newsletter!

Dodatkowo od razu otrzymasz ode mnie e-book o wartości 39 zł. To ponad 40 stron konkretów o nauce programowania i pracy w IT.

PS Zazwyczaj wysyłam 1-2 wiadomości na tydzień. Nikomu nie będę udostępniał Twojego adresu e-mail.