Opanowanie Async/Await: Zaawansowane obsługiwanie błędów i wzorce ponawiania
Async/await zrewolucjonizowało asynchroniczny JavaScript, ale poprawne obsługiwanie błędów wciąż stanowi wyzwanie. Ten artykuł analizuje solidną implementację, która obejmuje logikę ponawiania, obsługę limitów czasu, strategie awaryjne i wzorce odzyskiwania błędów niezbędne w aplikacjach produkcyjnych.

Wprowadzenie: Poza podstawowym Async/Await
Chociaż składnia async/await sprawia, że kod asynchroniczny jest czytelny, aplikacje produkcyjne wymagają zaawansowanej obsługi błędów, mechanizmów ponawiania, limitów czasu i strategii awaryjnych. Awaria sieci, limity API i błędy przejściowe wymagają odpornego kodu, który elegancko obsłuży niepowodzenia. Przeanalizujmy kompleksową implementację async/await, która rozwiązuje rzeczywiste problemy.
Oryginalny kod: typowe pułapki
Pierwotna implementacja zawierała kilka krytycznych problemów, które mogły powodować problemy w produkcji. Spójrzmy na poprawioną wersję i zrozummy, co było nie tak:
Poprawiony kod: odporny klient API
class APIClient { ... } // Pełny kod pominięty dla zwięzłościNaprawione krytyczne błędy
Oryginalny kod zawierał kilka poważnych błędów. Po pierwsze, AbortController był tworzony wewnątrz bloku try, ale musiał być dostępny w catch dla poprawnego czyszczenia. Po drugie, metoda isRetryableError mogła otrzymać null/undefined jako parametr, powodując błędy w czasie wykonywania — naprawiono poprzez dodanie sprawdzenia null. Po trzecie, AbortError nie był odróżniany od innych błędów, co mogło prowadzić do nieskończonych ponowień przy ręcznym anulowaniu. Po czwarte, obiekt error nie używał opcjonalnego łańcucha przy dostępie do error.message w handleFailure, co mogło spowodować awarię, gdy error było undefined.
Implementacja limitu czasu za pomocą Promise.race
Kod używa Promise.race, aby elegancko zaimplementować funkcję limitu czasu. Wyścig odbywa się między rzeczywistym żądaniem fetch a obietnicą timeout. Którekolwiek rozwiąże się lub odrzuci pierwsze, wygrywa. Gdy limit czasu zostanie przekroczony, wywoływane jest controller.abort(), aby anulować żądanie fetch, zapobiegając marnowaniu pasma i pamięci. Ten wzorzec jest lepszy niż zwykłe opakowanie fetch w timeout, ponieważ faktycznie anuluje żądanie zamiast je ignorować.
Wzrost wykładniczy z losowym jitterem
Metoda calculateBackoff implementuje wzrost wykładniczy — każde ponowienie oczekuje coraz dłużej (1s, 2s, 4s, 8s). Zapobiega to przeciążeniu serwera. Dodatkowy jitter (losowe opóźnienie) zapobiega problemowi „thundering herd”, gdy wielu klientów próbuje ponowić jednocześnie, co mogłoby spowodować kaskadowe awarie. To połączenie jest standardem branżowym w logice ponawiania w systemach rozproszonych i stosowane przez AWS, Google Cloud i główne API.
Inteligentna logika ponawiania z kontrolą bezpieczeństwa
Ulepszona metoda isRetryableError zawiera teraz niezbędne kontrole bezpieczeństwa. Najpierw weryfikuje, czy przynajmniej jeden parametr jest podany, aby zapobiec błędom null. Sprawdza wyraźnie AbortError i zwraca false — ręczne anulowania nigdy nie powinny być ponawiane. Metoda odróżnia błędy przejściowe warte ponowienia (awarie sieci, limity czasu, błędy serwera 5xx, ograniczenia 429) od błędów trwałych (400 bad request, 404 not found, 401 unauthorized). Inteligentne podejmowanie decyzji jest kluczowe w systemach produkcyjnych.
Rozszerzony kontekst błędów
Poprawiony kod próbuje analizować treść odpowiedzi błędu, dostarczając więcej informacji w przypadku awarii. Do rzuconych błędów dołączany jest obiekt response i sparsowany body, co umożliwia kodowi wywołującemu dostęp do szczegółowych informacji o błędzie. Użycie opcjonalnego łańcucha (?.) zapobiega awariom przy dostępie do potencjalnie niezdefiniowanych właściwości. To defensywne podejście zapewnia stabilność klienta nawet w przypadku nieoczekiwanych odpowiedzi API.
Strukturalna obsługa błędów
Zamiast po prostu rzucać błędy, ta implementacja zwraca strukturalne obiekty odpowiedzi z flagami sukcesu, danymi, komunikatami błędów i metadanymi, takimi jak liczba prób. Podejście to daje kodowi wywołującemu pełne informacje do podejmowania świadomych decyzji — wyświetlania przyjaznych użytkownikowi komunikatów, odpowiedniego logowania błędów lub uruchamiania alternatywnych procesów. Jednolity format zwracany niezależnie od powodzenia lub niepowodzenia upraszcza obsługę błędów w reszcie aplikacji.
Strategie awaryjne
Gdy wszystkie ponowienia zawiodą, handleFailure wdraża strategię łagodnej degradacji z prawidłowym obsługiwaniem null. Operator opcjonalnego łańcucha zapewnia, że kod nie zawiedzie, gdy error jest undefined. Jeśli dostępne są dane awaryjne (np. z wcześniejszego udanego żądania lub wartości domyślne), zwracane są zamiast całkowitej awarii. Umożliwia to aplikacjom działanie nawet podczas całkowitej niedostępności API. Flaga fromCache pozwala UI wskazać, że dane mogą być przestarzałe.
Wzorzec pętli for dla ponowień
Użycie pętli for z instrukcjami continue dla logiki ponawiania jest czytelniejsze niż podejścia rekurencyjne czy pętle while. Maksymalna liczba ponowień jest jawnie określona, zapobiega problemom z przepełnieniem stosu i jasno pokazuje przepływ. Licznik prób śledzi postęp i ułatwia logowanie. Przerwanie pętli przy napotkaniu błędów nie do ponowienia zapobiega niepotrzebnym opóźnieniom. Ten wzorzec jest bardziej czytelny i łatwiejszy w utrzymaniu niż alternatywy.
Zarządzanie zakresem AbortController
Poprawiony kod deklaruje AbortController na poziomie pętli, zapewniając dostęp w każdej iteracji. Pozwala to na prawidłowe anulowanie w scenariuszach limitu czasu i błędu. Tworzenie nowego AbortController dla każdej próby pozwala precyzyjnie kontrolować anulowanie żądań. Nowoczesny fetch z AbortController to standard dla anulowalnych operacji asynchronicznych w JavaScript, zastępujący starsze wzorce, takie jak tokeny anulowania Promise.
Pełne wsparcie metod HTTP
Poprawiona implementacja dodaje wygodne metody PUT i DELETE obok GET i POST, zapewniając pełne wsparcie operacji CRUD. Każda metoda prawidłowo konfiguruje typ żądania i, w razie potrzeby, serializuje body do JSON. Umożliwia to klientowi większą wszechstronność w rzeczywistych interakcjach z API, gdzie wszystkie metody HTTP są często potrzebne.
Ulepszony przykład użycia
Funkcja demonstracyjna zawiera teraz bardziej informacyjne wyjście konsoli z wizualnymi wskaźnikami (✓, ⚠, ℹ, ✗), ułatwiającymi przeglądanie logów. Flaga fromCache jest obsługiwana poprawnie, informując użytkowników, gdy dane mogą być przestarzałe. Obsługa błędów odróżnia odpowiedzi łagodnie zdegradowane od nieoczekiwanych błędów, zapewniając odpowiednią informację zwrotną w każdej sytuacji.
Uwagi dotyczące produkcji
Implementacje produkcyjne powinny dodać wzorce obwodów zabezpieczających, aby zapobiec kaskadowym awariom, limitowanie szybkości, aby przestrzegać kwot API, deduplikację żądań, aby uniknąć nadmiarowych wywołań, oraz szczegółowe typy błędów dla różnych scenariuszy awarii. Integracja z narzędziami monitorującymi pomaga śledzić wskaźniki ponawiania, wzorce awarii i metryki wydajności. Logika odświeżania tokenów uwierzytelniających często należy do tej warstwy. Rozważ użycie sprawdzonych bibliotek, takich jak axios z interceptorami lub ky.
Kluczowe wnioski
- Zawsze waliduj parametry w funkcjach sprawdzających błędy, aby zapobiec błędom null.
- Używaj opcjonalnego łańcucha (?.) przy dostępie do potencjalnie niezdefiniowanych właściwości, aby zapobiec awariom.
- Deklaruj AbortController w odpowiednim zakresie, aby zapewnić dostęp do czyszczenia.
- Rozróżniaj AbortError od innych błędów — ręczne anulowania nie powinny być ponawiane.
- Dołącz szczegółowy kontekst błędu (response, body) do rzucanych błędów dla lepszego debugowania.
- Promise.race elegancko implementuje timeouty, konkurując fetch z timeout promise.
- Wzrost wykładniczy z jitterem zapobiega przeciążeniu serwera i problemowi thundering herd.
- Strukturalne obiekty odpowiedzi z flagami sukcesu zapewniają spójne i informacyjne wyniki.
- Strategie awaryjne umożliwiają łagodną degradację podczas całkowitej awarii API.
- Pętle for zapewniają czytelną i bezpieczną logikę ponawiania, lepszą od rekurencji.
- Pełne wsparcie metod HTTP (GET, POST, PUT, DELETE) zwiększa wszechstronność klienta.
- Programowanie defensywne z kontrolą null i opcjonalnym łańcuchem zapobiega nieoczekiwanym awariom.