node.js - Å vente på to løfter i try/catch-blokk resulterer i "Ubehandlet løfteavvisning"


Jeg vil vente på to løfter som kjører parallelt. Jeg ønsker ikke å vente på hvert løfte serielt (noe som fungerer, men er tregere).

Av den grunn tenkte jeg at jeg kunne lage to løfter først for å få dem i gang, for eksempel to nettverksforespørsler, deretter vente på dem og kunne fange opp feil i en catch-blokk. Den antagelsen ser ut til å være feil, da jeg får en advarsel når jeg kjører dette eksempel-koden.

  • Hvorfor er det?
  • Hvordan kjører jeg best to eller flere nettverksforespørsler parallelt på en elegant måte?
  • Hvorfor advarer ikke Typescript meg om at catch-blokken ikke vil fange avvisninger?
async function testMultipleAwait() {
  try {
    const aPromise = new Promise((resolve) => {
      setTimeout(() => resolve('a'), 200);
    });

    const bPromise = new Promise((_, reject) => {
      setTimeout(() => reject('b'), 100);
    });

    const a = await aPromise;
    const b = await bPromise;
  } catch (e) {
    console.log('Caught error', e);
  }
}

testMultipleAwait();

Resulterer IKKE i "Caught error"-utdata, i stedet får jeg

tsc test-try-catch-await.ts && node test-try-catch-await.js

(node:31755) UnhandledPromiseRejectionWarning: b
(node:31755) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:31755) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
Caught error b
(node:31755) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

Svar #1

Jeg tror jeg vet hva problemet er. Selv om løftene starter på en parallell måte, venter du fortsatt på aPromise og bPromise sekvensielt:

const a = await aPromise; // First await this...
const b = await bPromise; // and then start to await this

Dette er ikke så mye et problem når begge løfter oppfylles. Det ville holde deg ventende omtrent like lenge som Promise.all ville, og deretter fortsette lykkelig. Derfor er ikke dette problemet så åpenbart i det hele tatt...

Det er viktig å vite at under panseret blir denne try-catch-blokken konvertert til et promise, på grunn av async-await. Det betyr at alt som kommer etter setningen som først awaites, vil ende opp i en then-tilbakekallingsfunksjon.

Så, const b=await bPromise vil ikke kjøre før const a har ankommet (etter 200 ms). bPromise feiler 100 ms tidligere.

Dette er ikke for å si at async-await ikke fanger opp feilen eller kobler på catch-blokken din i det hele tatt. Tross alt finnes det er terminalutdata av både node-advarselen og catch-håndtereren din:

tsc test-try-catch-await.ts && node test-try-catch-await.js

1 first node sees the error     > (node:31755) UnhandledPromiseRejectionWarning: b
2                                 (node:31755) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
3                                 (node:31755) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
4 and then your catch handler   >      Caught error b
5                                 (node:31755) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

Så catch-delen fungerer, men async-funksjonen gidder ikke å koble den til bPromise før etter minst 200 ms. Linje 5 ser ut til å bekrefte dette:

PromiseRejectionHandledWarning: Promise rejection was handled asynchronously.

Avvisningsfeil kastes så snart mikrotasks-køen er tom.

Avvisningen av løftet ble håndtert, men Node mener du er for sen. Du kan fikse dette problemet ved å bruke Promise.all. På denne måten await du én gang, og din async-funksjon vil fange opp alle potensielle feil først.

// Everything just as it is.. and then:

const [a, b] = await Promise.all([
    aPromise, 
    bPromise,
]);

Fordi jeg var nysgjerrig skrev jeg inn koden din i Chrome-konsollen for å se hva som ville skje. En feillogg dukker opp i en veldig kort periode (jeg antar 100 ms). Når man ser på denne outputen kan man nesten høre Chrome si:

"Å, vent! Det blir tatt likevel. Her er meldingen!"

Klikk for GIF-animasjon.

Svar #2

En fin måte å vente på at flere Promise-er blir løst på, er å bruke Promise.all-funksjonen. Den forventer et Array av Promise-er, og produserer et Promise som løses til et Array som inneholder verdiene de individuelle Promise-ene ble løst til. Videre løses det bare etter at den siste Promise er løst. Hvis noen av input-Promise-ene avvises, avvises hele Promise.all-uttrykket også. Den kjører effektivt alle sine inputprosesser "på samme tid", og etterligner det klassiske "fork-join"-mønsteret.

Grunnen til at du får den feilen, er på grunn av at timeout-prosessen din starter så snart du definerer de Promise-ene, og ikke når du await dem, flere linjer senere. Du kan se dette hvis du logger litt tekst i definisjonen av en av Promise-ene, og deretter logger noe annet før dine await-uttrykk:

async function testMultipleAwait() {
  try {
    const aPromise = new Promise((resolve) => {
      console.log('starting timeout');
      setTimeout(() => resolve('a'), 200);
    });

    const bPromise = new Promise((_, reject) => {
      setTimeout(() => reject('b'), 100);
    });

    console.log('awaiting');
    const a = await aPromise;
    const b = await bPromise;
  } catch (e) {
    console.log('Caught error', e);
  }
}

testMultipleAwait();

// logs:
// "starting timeout"
// "awaiting"

For å løse dette umiddelbare problemet, kan du konvertere de Promiseene til funksjoner, og deretter kalle dem, eller await dem "umiddelbart":

// Option 1
async function testMultipleAwait() {
  try {
    const aPromise = () => new Promise(resolve => {
      setTimeout(() => resolve("a"), 200);
    });

    const bPromise = () => new Promise((_, reject) => {
      setTimeout(() => reject("b"), 100);
    });

    const a = await aPromise();
    const b = await bPromise();
  } catch (e) {
    console.log("Caught error", e);
  }
}

testMultipleAwait();
// Option 2
async function testMultipleAwait() {
  try {
    await new Promise(resolve => {
      setTimeout(() => resolve("a"), 200);
    });

    await new Promise((_, reject) => {
      setTimeout(() => reject("b"), 100);
    });
  } catch (e) {
    console.log("Caught error", e);
  }
}

testMultipleAwait();

For å samle dette alt sammen, for å få dem til å kjøre parallelt, kan du prøve noe som dette:

async function testMultipleAwait() {
  try {
    await Promise.all([
      new Promise(resolve => {
        setTimeout(() => resolve("a"), 200);
      }),
      new Promise((_, reject) => {
        setTimeout(() => reject("b"), 100);
      })
    ]);
  } catch (e) {
    console.log("Caught error", e);
  }
}

testMultipleAwait();

Relaterte artikler