Pułapka zwracania Promise
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.