Pułapka zwracania Promise

  • javascript
  • async
  • typescript

Zwrócenie wartości z rozwiązanej Promise nie jest tym samym co zwrócenie samej Promise. Spójrz na poniższy przykład i pomyśl, jaki wynik zwrócą obie funkcje.

function asyncName(name) {
  return new Promise((resolve) => {
	  setTimeout(() => resolve(name), 1000);
  });
}

function getName() {
  return asyncName("Przemek");
}

async function getResolvedName() {
	return await asyncName("Przemek");
}

W pierwszym i drugim przykładzie zostanie zwrócony string, czyli moje imię “Przemek”. Czyli bez znaczenia w jaki sposób napiszemy kod.

W kontekście systemu typów TypeScript, również nie zauważymy różnicy ponieważ Return Type definiujemy w taki sam sposób.

function getName(): Promise<string> {
  return asyncName("Przemek");
}

await getName() // => "Przemek"

async function getResolvedName(): Promise<string> {
	return await asyncName("Przemek");
}

await getResolvedName() // => "Przemek"

Jednak diabeł tkwi w szczegółach, oraz gdy wyjdziemy poza Happy Path. Spójrz na kolejny przykład, tylko tym razem funkcja asynchroniczna będzie zwracać błąd.

function asyncName(name) {
  return new Promise((_, reject) => {
    // Wywołujemy `reject` w celu symulacji błędu.
    setTimeout(() => reject(name), 1000);
  });
}

function getName() {
  try {
    return asyncName("Przemek");
  } catch {
    console.error("Handled error");
    return null;
  }
}

async function getResolvedName() {
  try {
    return await asyncName("Przemek");
  } catch {
    console.error("Handled error");
    return null;
  }
}

W tym momencie trafiamy na pułapkę, która związana jest z await oraz przechwytywaniem błędów. Gdy wywolamy obie dwie funkcje otrzymamy następujący wynik:

getName(); // -> Uncaught Przemek

getResolvedName(); // -> Handled error

Dlaczego tak się dzieje? await służy do tego, żeby zatrzymać wykonanie kodu do momentu rozwiązania Promise. Więc gdy użyjemy await, błąd zostanie przechwycony przez blok try...catch, dzięki czemu możemy go bezpiecznie obsłużyć.

Gdy tego nie zrobimy funkcja Promise po prostu opuści funkcję getName, co za tym idzie, wydostanie się ona z bloku try...catch i zostanie rzucona jako wyjątek w nieobsłużonym bloku kodu.

To jak żyć?

Nie chcę narzucać sposobu pisania kodu. Najważniejsze jest, żebyś zrozumiał rożne podejścia i ich konsekwencje. Można sprowadzić to do tego, że warto zawsze używać await, gdy nasz kod wykonuje się w try...catch. W innych przypadkach może być to po prostu zbędne. Najprostrzym przykładem może być abstrakcja na fetch taka jak w poniższym przykładzie:

function someFetchAbstraction(url) {
  return fetch(url, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
  });
}

// ...

try {
  const response = await someFetchAbstraction("https://api.example.com/data");
} catch (error) {
  console.error("Error fetching data:", error);
}

Jak widać fetch został opakowany w dodatkowe parametry, żeby zdefiniować jakąś bazowy kontrakt z komunikacją z API. Nierozwiązana Promise celowo jest zwrócona, żeby mogła być obsłużona w innym miejscu np. w jakimś serwisie, który posiada logikę biznesową.

Dobre praktyki są ważne, jednak jeszcze ważniejsze jest zrozumienie, jak działa JavaScript. Mam nadzieję, że tym krótkim wpisem będziesz bardziej świadom tego zachowania i ustrzeżesz się prostych ale ciężkich do znalezienia błędów.