Strona główna / Artykuły

Await widzi tylko jedną obietnicę: projektowanie API typu Async/Await, które samodzielnie realizują swoje zadania

Co tak naprawdę obserwuje JavaScript async/await, dlaczego praca odseparowana, callbacki, strumienie oraz Promise.all omijają jego kontrolę, oraz jak projektować Promises, które oznaczają zakończenie pracy.

Wydawca: REACTAPP.TOP13 min czytania3003 słów

Źródła: 90% of Developers Don’t Understand What JavaScript `await` Actually Waits For

Latwo odczytać await jako „czekaj, aż ta funkcja się zakończy”, jednak środowisko wykonawcze daje znacznie węższe obietnice: ewaluuje jedno wyrażenie, traktuje jego wynik jako wartość przypominającą Promise i wstrzymuje bieżącą funkcję async aż ta pojedyncza wartość zostanie ustalona. Wszystko inne zależy od tego, w jaki sposób powstała ta wartość. To rozbieżność tłumaczy, dlaczego kod z starannie umieszczonymi słowami kluczowymi await może nadal zwracać wynik zbyt wcześnie, działać wolniej niż to konieczne, ignorować błędy lub korzystać ze starych danych. Po przeanalizowaniu poniższych przypadków będziesz mógł spojrzeć na dowolne await i zadać jedno kluczowe pytanie: czy ten Promise rzeczywiście reprezentuje pracę, która mnie interesuje?

Problem: kod oczekujący na wykonanie, który wygląda na zakończony, ale takim nie jest

Błędy w tym obszarze wykazują pewien wzorzec. Programista czeka na funkcję i zakłada, że wszystko, co zostało przez nią rozpoczęte, zostało zakończone. Czeka na metodę tablicy i zakłada, że każdy asynchroniczny callback został wywołany. Czeka na ustawienie timera, subskrypcję na zdarzenie lub zapis do strumienia i sądzi, że podstawowa operacja jest zakończona. Kod działa poprawnie, ponieważ await znajduje się właśnie tam.

Jednak await nie ma pojęcia o „zadaniu”, „żądaniu” ani „wszystkim, co rozpoczęła ta funkcja”. Jeśli wyrażenie po jego prawej stronie nie obejmuje całego cyklu życia zadania, żaden element tego słowa kluczowego nie może to objąć. Mając to na uwadze, zmienia się sposób projektowania API, pętli, zadań w tle, kodu opartego na zdarzeniach oraz obsługi błędów.

Wywołanie następuje przed czekaniem

Weźmy najzwyklejszy wiersz w asynchronicznym JavaScriptu.

const user = await loadUser();

W czasie wykonywania kodu await nie jest najpierw oceniane, a dopiero potem uprzejmie proszone o uruchomienie loadUser asynchronicznie. Funkcja loadUser() jest oceniana dokładnie tak samo jak każde inne wezwanie funkcji. To wezwanie zwraca wartość, która może być natywnym obiektem Promise, dostosowanym obiektem typu thenable, zwykłym obiektem, łańcuchem znaków, wartością undefined lub czymkolwiek innym. Dopiero wtedy await może przetworzyć uzyskany wynik.

Skutkiem tego jest to, że rzeczywista operacja zazwyczaj rozpoczyna się już podczas wezwania, zanim nastąpi jakiekolwiek oczekiwanie. Jeśli wywołasz dwie funkcje asynchroniczne i zachowasz ich obiekty Promise przed użyciem await, obie operacje rozpoczną się niemal jednocześnie:

const userPromise = loadUser();
const settingsPromise = loadSettings();

const user = await userPromise;
const settings = await settingsPromise;

Tutaj loadUser() rozpoczyna swoją pracę i zwraca Promise, a następnie loadSettings() rozpoczyna swoją. Dwie linie z await jedynie zbierają wyniki w miarę ich dostępności.

Porównaj to z wersją, którą większość ludzi pisze domyślnie:

const user = await loadUser();
const settings = await loadSettings();

Teraz loadSettings() nie jest nawet wywoływany, dopóki pierwsza obietnica nie zostanie zrealizowana, a funkcja nie wznowi działania. Dwie żądania są wykonywane jedno po drugim.

Zauważ, że sam await nie podejmował żadnej decyzji dotyczącej paralelizmu – decyzję tę podejmowało położenie wywołań funkcji. Zwięzły sposób na zapamiętanie tego: wywołanie funkcji asynchronicznej rozpoczyna operację, natomiast await decyduje, kiedy aktualna funkcja ma kontynuować działanie. Jedna uwaga dotycząca pierwszego wzorca: jeśli settingsPromise zostanie odrzucony, podczas gdy nadal czekasz na userPromise, to odrzucenie to zostanie na chwilę zignorowane, co niektóre środowiska raportują jako błąd. Promise.all unika tego problemu, obserwując oba Promises jednocześnie. Aby uzyskać bardziej szczegółowe porównanie tych podejść, zobacz wybór między Promise.all, Promise.race a sekwencyjnymi await.

Rozstrzygnięcie jednego Promise, a nie wszystkich efektów ubocznych

Rozważmy funkcję tworzenia konta, która przechowuje rekord, wysyła e-mail powitalny bez czekania na jego odpowiedź ani zwracania jej, a następnie zwraca konto.

async function createAccount(input) {
  const account = await accountRepository.save(input);

  sendWelcomeEmail(account.email);
  return account;
}

Następnie osoba dzwoniąca pisze oczywisty tekst:

const account = await createAccount(input);

Ten wywołanie jest kontynuowane, gdy Promise zwrócone przez createAccount zostanie spełnione, co następuje po zakończeniu zapisu i zwrocie wartości przez funkcję. Nie podaje ono żadnych informacji na temat adresu e-mail. sendWelcomeEmail utworzyło oddzielną operację asynchroniczną, której Promise nigdy nie zostało powiązane z tym zwróconym przez createAccount, więc z punktu widzenia wywołującego ta operacja nie istnieje.

To jest sedno nieporozumienia. await śledzi jedynie łańcuch Promises dostępnych od wartości, którą otrzymało. Nie przeszukuje wszystkiego, co funkcja rozpoczęła, i z pewnością nie czeka, aż proces stanie w miejscu.

Praktyczne konsekwencje są trzy:

  • Jeśli Promise związany z e-mailem zostanie odrzucony, blok try/catch w wywołującym nigdy go nie zauważy.
  • Jeśli proces zostanie zakończony, e-mail może po prostu nigdy nie zostać wysłany.
  • Jeśli obsługa HTTP odpowiada pomyślnie, to sukces nie mówi nic pewnego na temat zadania wykonywanego w tle.
  • To wszystko nie oznacza, że każdy efekt uboczny musi zostać zakończony w ramach żądania. Wiele zadań rzeczywiście powinno być wykonywanych w tle. Prawdziwym problemem jest niejasna odpowiedzialność. Zadania istotne powinny być zwracane i oczekiwane, wyraźnie oznaczane jako opcjonalne i obsługiwane w taki sposób, lub przekazywane do trwałej kolejki lub systemu zadań. Tworzenie obiecywania (Promise) i pozostawienie go bez dalszej obsługi to nie przetwarzanie w tle – to jedynie nadzieja.

    Funkcja asynchroniczna może zostać zakończona, podczas gdy jej podfunkcje wciąż są w trakcie wykonywania

    Inną założeniem jest to, że słowo kluczowe async sprawia, iż JavaScript śledzi każdą operację asynchroniczną rozpoczętą wewnątrz funkcji. Tak nie jest. Funkcja async tworzy dokładnie jedno obietnictwo (Promise), a to obietnictwo jest powiązane wyłącznie z tym, co funkcja wyraźnie zwraca lub na co czeka w ramach swojego własnego przepływu sterowania. Wszystko inne może nadal działać po tym, jak to obietnictwo zostanie rozstrzygnięte.

    Klasyczny przykład wykorzystuje metodę forEach:

    async function processUsers(users) {
      users.forEach(async (user) => {
        await updateUser(user);
      });
    }
    

    Każde wywołanie asynchronicznego callbacka zwraca obiekt Promise, ale forEach wyrzuca te wartości zwracane. Nie zbiera ich, nie czeka na nie ani ich nie udostępnia; zwraca undefined. Dlatego processUsers osiąga swój zamykający nawias niemal natychmiast, a ponieważ funkcja async, która się w nim znajduje, kończy się wartością undefined, wywołujący kontynuuje działanie na długo przed zakończeniem jakiejkolwiek aktualizacji:

    await processUsers(users);
    console.log("All users processed");
    

    Twierdzenie w logu jest po prostu błędne. Wywołujący czekał na zakończenie planowania funkcji zwrotnych przez processUsers, a nie na wykonanie tych funkcji.

    Aby to naprawić, obietnice potomne muszą zostać połączone z operacją nadrzędną. W zależności od sytuacji oznacza to użycie pętli for...of z await wewnątrz dla ścisłego sekwencjonowania, Promise.all(users.map(updateUser)) dla pełnego równoległości lub ogranicznika współbieżności, gdy zbiór może być na tyle duży, że przeładowałby bazę danych lub API. Ogólniejsze zasady obejmują znacznie więcej niż tylko forEach: obietnica nadrzędna czeka tylko na prace potomne, które są częścią jej łańcucha. Narzędzia takie jak zasady no-floating-promises i no-misused-promises w typescript-eslint automatycznie wykrywają wiele z tych błędów.

    API funkcji zwrotnych są niewidoczne dla await

    Znaczna część ekosystemu JavaScript powstała przed Promises i informuje o zakończeniu działania za pomocą funkcji zwrotnych. To prowadzi do kodu w takim wyglądzie:

    await setTimeout(() => {
      console.log("Timer completed");
    }, 1000);
    

    Nie robi pauzy nawet na sekundę. Funkcja setTimeout rejestruje funkcję odwołania i natychmiast zwraca identyfikator timera (liczbę w przeglądarkach, obiekt Timeout w Node.js). Ten identyfikator nie jest obietnicą (Promise) opisującą moment uruchomienia timera. Funkcja await otrzymuje zwykłą wartość, traktuje ją jako już dostępną i kontynuuje wykonywanie funkcji w następnym mik zadaniu, długo przed uruchomieniem funkcji odwołania.

    To samo problem występuje przy słuchaczach zdarzeń, starszych interfejsach systemu plików, sterownikach bazy danych, strumieniach oraz samodzielnie opracowanych SDK. Jeśli funkcja zwraca subskrypcję, identyfikator żądania, wartość logiczną lub emiter zdarzeń, oczekiwanie na tę wartość zwróconą nie przekształca ewentualnej funkcji odwołania w zakończenie obietnicy (Promise).

    Aby móc użyć mechanizmu await dla zachowań opartych na funkcjach odwołania, potrzebujesz obietnicy (Promise), która rozwiązuje się lub odrzuca dokładnie w momencie, gdy funkcja odwołania zgłasza wynik:

    function delay(milliseconds) {
      return new Promise((resolve) => {
        setTimeout(resolve, milliseconds);
      });
    }
    
    await delay(1000);
    

    Teraz oczekiwana wartość rzeczywiście odnosi się do zdarzenia, które nas interesuje: Promise pozostaje w stanie oczekiwania, dopóki timer nie wywoła metody resolve. Nowoczesne platformy często już dostarczają takie otulacze; Node.js na przykład oferuje funkcję setTimeout opartą na Promise w modułach node:timers/promises oraz util.promisify dla funkcji w stylu callback.

    To ujawnia pytanie projektowe kryjące się za każdym await: czy zwrócona wartość oznacza zakończenie, czy jedynie rejestrację, uruchomienie lub wskaźnik do operacji wciąż trwającej? Jeśli chodzi o jedną z tych ostatnich opcji, oczekiwanie na nią nie zapewni uzyskania wyniku, który sobie wyobrażamy.

    Thenables, zwykłe wartości i granica mikrozadań

    await nie wymaga prawdziwej instancji Promise. Jeśli wartość jest obiektem typu thenable, czyli obiektem posiadającym metodę .then nadającą się do wywołania, await przyjmuje sposób realizacji tego obiektu. Dzięki temu biblioteki kompatybilne z Promise oraz własne obiekty asynchroniczne mogą być bezpośrednio używane w funkcjach async, a obiekty te mogą zachowywać się asynchronicznie bez konieczności używania new Promise.

    Dla wartości, które nie są thenable, await nadal wprowadza krok asynchroniczny:

    async function example() {
      console.log("before");
      await 42;
      console.log("after");
    }
    
    console.log("start");
    example();
    console.log("end");
    

    To wyświetla start, before, end, a dopiero potem after, mimo że 42 było dostępne od razu. Funkcja zwraca wartość w punkcie await, kod synchroniczny po wezwaniu kontynuuje działanie, a pozostała część example jest wykonywana później jako mikrozadanie.

    Zatem oczekiwanie na zwykłą wartość nie jest synchroniczne. Może zmienić kolejność wykonywania kodu w stosunku do otaczających go instrukcji synchronicznych oraz innych zaimprowizowanych mikrozadań, a w ścieżkach wrażliwych na kolejność lub intensywnie wykorzystywanych niepotrzebne await tworzą dodatkowe bariery planowania. Opóźnienie to jest zazwyczaj znikome pod względem wydajności, ale koncepcja pozostaje ta sama: kod po await nigdy nie kontynuuje działania na tym samym, nieprzerwanym stosie wywołań.

    Odrzucenie jest częścią procesu rozstrzygania

    Ludzie często mówią, że await czeka, aż Promise zostanie rozwiązany. Dokładniej jest powiedzieć, że czeka, aż Promise zostanie ustalony, a ustalenie obejmuje również odrzucenie. Gdy oczekiwany Promise zostanie odrzucony, await wyrzuca powód odrzucenia wewnątrz bieżącej funkcji async, co umożliwia używanie zwykłych struktur try/catch do radzenia sobie z awariami asynchronicznymi:

    try {
      const order = await loadOrder(orderId);
      return order;
    } catch (error) {
      logger.error(error);
      throw error;
    }
    

    Ta ochrona obowiązuje tylko wtedy, gdy odrzucające się Promise jest faktycznie oczekiwane wewnątrz bloku try. Promise utworzone i pozostawione bez nadzoru całkowicie unika tej ochrony:

    try {
      loadOrder(orderId);
    } catch (error) {
      console.error("This does not catch the Promise rejection");
    }
    

    Tutaj sama próba połączenia zakończyła się pomyślnie: zwróciła Promise bez rzucania błędu. Odrzucenie następuje później, poprzez ten Promise, już po tym, jak blok try dawno zakończył swoją pracę.

    To jest szczególnie niebezpieczne, gdy await jest celowo pomijany, aby coś było „nieblokujące”, podczas gdy programista wciąż uważa, że otaczające je try/catch chronią to rozwiązanie. Tak nie jest. Każda operacja asynchroniczna wymaga wyraźnego właściciela w przypadku jej niepowodzenia: osoby, która na nią czeka lub ją zwraca, wyraźnego obsługiwanego elementu .catch lub systemu zdolnego przejąć odpowiedzialność. Promise bez właściciela nie oznacza szybszej architektury – to ścieżka błędu odcięta od kodu, który rozumie, co poszło nie tak. Artykuł na temat tego, co naprawdę gwarantuje async/await omawia tę samą koncepcję właściciela z perspektywy obsługi błędów.

    Promise.all koordynuje wyniki, a nie rezultaty

    Promise.all jest zazwyczaj opisywany jako „czekanie na kilka Promises jednocześnie”. To przyzwoita pierwsza aproksymacja, ale istotne są szczegóły. Zwraca nowy Promise, który zostaje spełniony, gdy wszystkie wejścia zostaną spełnione, a odrzucony natychmiast, gdy choć jedno z wejść zostanie odrzucone.

    const results = await Promise.all(tasks);
    

    Nawet tutaj await ma jeden cel: zbiór obietnic stworzony przez Promise.all.

    Gdy jedno z zadań zawiedzie wcześnie, cały zbiór zawodzi wraz z nim, a funkcja przechodzi ścieżkę obsługi błędu. Pozostałe zadania nie są anulowane – kontynuują działanie i mogą nadal powodować efekty uboczne, nawet po tym, jak wywołujący już uznał całą grupę za nieudaną. Wyobraź sobie grupę zadań, które uruchamiają kilka zewnętrznych aktualizacji: jedna zawodzi natychmiast, wywołujący zgłasza „grupa zadań nieudana”, podczas gdy pozostałe aktualizacje pomyślnie się kończą, pozostawiając system częściowo zmieniony.

    Promise.all umożliwia koordynację wyników, ale nie obsługę transakcji ani anulowania zadań. Wybierz narzędzie odpowiadające potrzebnym semantykom:

    • Jeśli dopuszczalny jest częściowy sukces, zebraj wszystkie wyniki (na przykład za pomocą Promise.allSettled) i sprawdź każdy z nich.
    • Jeśli potrzebujesz zachowania typu wszystko albo nic, operacje podstawowe wymagają rzeczywistego mechanizmu cofania lub rekompensaty.
    • Jeśli pozostała praca powinna zostać zatrzymana po pierwszej awarii, każda operacja musi umożliwiać anulowanie, zazwyczaj za pomocą wspólnego AbortSignal.

    Łączna obietnica (Promise) nie może odwołać działań, które już się rozpoczęły. Jej realizacja oznacza tylko to, co pozwala na to jej struktura.

    Strumienie i zasoby działające przez długi czas

    Strumienie są kolejnym częstym źródłem zamieszania. Metoda write strumienia może zwrócić wartość logiczną wskazującą, czy wewnętrzny bufor może przyjąć więcej danych. Wywołanie .pipe() zwraca strumień docelowy. Parser oparty na zdarzeniach może zwrócić wynik już po podłączeniu słuchaczy. Oczekiwanie na którakolwiek z tych wartości nie oznacza czekania, aż strumień zakończy pracę, wypłucze dane, zamknie się lub zapisa każdy bajt.

    Zakończenie jest zazwyczaj sygnalizowane za pomocą wydarzeń takich jak finish, end, close lub error, a aby móc na nie w sposób niezawodny poczekać, te wydarzenia muszą zostać przekształcone w Promise. Wiele bibliotek, a także sam Node.js poprzez pipeline w modulu node:stream/promises, oferuje narzędzia, które tworzą jeden Promise, którego realizacja obejmuje cały pipeline, włączając w to jego błędy.

    Ta zasada odnosi się do każdego zasobu o długim czasie życia:

    • Rozpoczęcie przesyłania pliku to nie jego zakończenie.
    • Otwarcie połączenia to nie zakończenie zapytania.
    • Umieszczenie zadań w kolejce to nie jego wykonywanie.
    • Dodanie wiadomości do kolejki to nie jej przetwarzanie.

    Wartość zwracana przez API określa to, co await może zaobserwować. Zanim zaufasz await, sprawdź, który punkt życiowy jego Promise faktycznie oznacza.

    Zakończenie nie oznacza istotności

    Wyobraź sobie pole wyszukiwania, w którym użytkownik wpisuje „JavaScript”, a następnie szybko zmienia to na „TypeScript”. W tym momencie wysyłane są dwa żądania. Żądanie dotyczące TypeScript wraca pierwsze i na ekranie pokazują się odpowiednie wyniki; dopiero potem przychodzi wolniejsza odpowiedź od JavaScript i je zastępuje.

    Każda funkcja w tym procesie używała poprawnie await. Błąd występuje dlatego, że oczekiwanie i istotność to dwie odrębne kwestie. await wznowia działanie, gdy Promise zostanie rozstrzygnięty; nie sprawdza, czy ktoś nadal chce ten wynik, czy nowsze żądanie go zastąpiło, ani czy komponent teraz pokazuje inny rekord. Kod wyglądający sekwencyjnie jedynie uporządkowuje kroki w ramach jednej wywołania, a kilka wywołań tej samej funkcji może nadal swobodnie się nakładać.

    Rozwiązanie tego problemu wymaga wyraźnego określenia, która operacja jest aktualna. Można anulować przestarzałe żądania, porównywać każdą odpowiedź z identyfikatorem najnowszego żądania, stosować sprawdzanie wersji lub polegać na warstwie pobierania danych, która klasyfikuje odpowiedzi według parametrów, które je wygenerowały. await nie może określić, który wynik należy do bieżącej sytuacji. Aby zapoznać się z przewodnikiem skupionym na React, zobacz jak naprawić problemy związane z konkurencyjnością, których nie da się rozwiązać za pomocą debouncingu.

    Fakt realizacji żądania nie oznacza, że system jest spójny

    Zapis do bazy danych może się udać, mimo że repliki wciąż są w tyle. Publikacja wiadomości zazwyczaj zostaje rozwiązana po tym, jak broker ją przyjmuje, a nie po tym, jak konsument ją przetworzy. Wysyłanie e-maila zwykle kończy się pomyślnie, gdy dostawca przyjmuje żądanie, a nie w momencie, gdy ktoś go otrzymuje. Łatwo interpretować funkcję await jako dająca większą ilość potwierdzeń, niż faktycznie dostarcza API.

    await publishEvent(event);
    

    Ta linia może zapewnić, że zdarzenie dotarło do brokera. Nie oznacza to koniecznie, że projekcje, powiadomienia lub indeksy wyszukiwania poniżej w łańcuchu przetwarzania nadrobiły opóźnienie.

    await sendEmail(message);
    

    Podobnie może to oznaczać jedynie, że dostawca przyjął wiadomość. Nie gwarantuje to jej dostarczenia, przeczytania przez odbiorcę, ani nawet przechowania jej po późniejszej obróbce.

    To, co oznacza „zrealizowane”, jest określone w umowie API tworzącego Promise, a await nie może wzmocnić tej umowy. Problemy pojawiają się, gdy kolejny fragment kodu zakłada, że już nastąpiła szersza zmiana stanu – na przykład endpoint publikujący zdarzenie i natychmiast wysyłający zapytanie do modelu do odczytu utrzymywanego przez konsumenta, oczekując, że nowe dane już tam będą. Promise zostało prawidłowo zrealizowane; architektura po prostu jeszcze nadąża. W zależności od produktu odpowiednia architektura może zwracać status w oczekiwaniu, korzystać z modelu do zapisu zamiast do odczytu, czekać na wyraźne potwierdzenie lub przyjąć koncepcję ewentualnej spójności w interfejsie użytkownika.

    Projektowanie Promise, które odpowiadają swoim nazwom

    Większość zamieszania związanych z await znika, gdy staje się nawykiem jedno pytanie: czy wartość, na którą czekam, oznacza zakończenie całej operacji, która mnie interesuje? Przyjrzyjmy się wcześniejszym przypadkom – odpowiedź jest wyraźnie „nie”, gdy:

    • funcja tworzy potomne Promises bez ich zwracania lub oczekiwania
    • metoda tablicy odrzuca Promises zwracane przez jej asynchroniczne callbacki
    • API zwraca wskaźnik i informuje o zakończeniu poprzez wydarzenia
    • nadawca rozwiązuje problem przed tym, jak odbiorcy wykonają swoją część

    Obecność await dowodzi jedynie tego, że aktualna funkcja została wstrzymana, dopóki jakaś wartość przypominająca Promise nie została ustalona. Nie dowodzi to jednak, że powiązane zadania zostały zakończone, że efekty uboczne wystąpiły dokładnie raz, że poprzednie zadania zostały anulowane, że błędy pozostały powiązane, ani że wynik jest nadal aktualny.

    Dlatego właśnie prawidłowe implementowanie kodu asynchronicznego to w dużej mierze problem projektowania API. Obietnica (Promise), którą zwraca funkcja, powinna zostać spełniona w momencie odpowiadającym zadaniu, które określa jej nazwa. Funkcja o nazwie saveDocument powinna jasno wskazywać, czy jej Promise dotyczy lokalnej persistencji, zdalnej synchronizacji, czy obu tych elementów. Funkcja o nazwie scheduleReport może uzasadnienie rozwiązać się po przyjęciu zlecenia zamiast po zakończeniu raportu, ale jej nazwa oraz typ zwracanych wartości powinny to jasno pokazywać, na przykład poprzez rozwiązanie się z identyfikatorem zlecenia, który może być sprawdzany lub śledzony przez wywołującego.

    Co może, a czego nie może obiecywać await

    • await ewaluuje jedną wyrażenie i czeka na ustalenie się tej pojedynczej wartości; nie wie nic o „zadaniu” jako całości.
    • Miejsce, w którym wywołujesz funkcje asynchroniczne, decyduje o tym, czy praca będzie wykonywana równolegle; await określa jedynie moment wznowienia działania.
    • Praca, która nie jest zwracana ani oczekiwana, jest odseparowana: jej błędy, czas wykonywania oraz zakończenie są niewidoczne dla wywołującego.
    • API do callbacki, zdarzeń i strumieni muszą być otoczone Promises, które zapewnią rzeczywiste zakończenie, zanim await będzie miało sens.
    • Promise.all ani nie anuluje, ani nie cofa operacji; wybierz allSettled, mechanizm kompensacji lub AbortSignal w zależności od potrzebnych semantyk.
    • Zakończenie, istotność i spójność to trzy różne gwarancje, a tylko pierwsza pochodzi od await.
    • Traktuj każdy zwracany Promise jak umowę i upewnij się, że jego zakończenie oznacza dokładnie to, co sugeruje nazwa funkcji.

    Literatura pokrewna

  • Serializuj prace asynchronne w JavaScript za pomocą mutexa promise: Wykorzystaj łańcuch obietnic AsyncLock, aby nakładające się żądania klienta wykonywały się w kolejności FIFO, a stan interfejsu pozostawał uporządkowany bez użycia rdzennych zamykań wątków.