javascript - Under vilka omständigheter skulle en finally-sats inte nås?


Förutom de vanliga misstänkta (process.exit(), eller processavslut/signal, eller krasch/hårdvarufel), finns det några situationer där kod i en finally-block inte kommer att nås?

Följande TypeScript-kod körs vanligtvis som förväntat (med node.js) men avslutas ibland omedelbart vid rad 4 utan att några undantag uppstår eller någon förändring sker i processens utgångskod (avslutar 0/framgång):

1 import si from 'systeminformation';
2 async populateResolvedValue() {
3   try {
4     const osInfo = await si.osInfo();
5     ...
6   } finally {
7     console.log('whew!');  //  <=========== NOT REACHED!
8   }
9 }

Jag har verifierat detta inom en IJ-debuggsession - finally-blocket på rad 7 kommer ibland inte att köras och avslutas omedelbart vid rad 4 (med en viss avveckling av stacken). Det enda fallet jag känner till där detta kan hända (och fortfarande avslutas framgångsrikt) är om ett segmentation fault uppstår någonstans inom someAsyncFunc(), men jag lade till "segfault-handler" och inget dök upp där.

Jag har också försökt detta med Promise.then/finally istället för async/await med try/finally-semantik - exakt samma beteende.

node.js: v12.18.2 och v14.16.0

Svar #1

Tack vare alla kommentarer kunde jag felsöka huvudproblemet och hitta ett tillfredsställande svar.

Kort sagt, ja - förutom de vanliga misstänkta, kan ett Promise.finally (eller try/finally med await) block aldrig nås och köras om det aktuella löftet aldrig “avgörs”. Vissa löften kan ta mycket längre tid att avgöra än väntat (I/O-väntan, etc.) och så småningom lösas eller avvisas, men om ett löfte aldrig avgörs (på grund av ett fel eller avsiktligt) kommer all kod som beror på det löftet (then, finally, etc.) aldrig att köras.

Så vad är ett osläppt löfte, och hur uppstår de? Ett löfte kanske aldrig löses om dess resolve- eller reject-återuppringningsfunktioner aldrig anropas (på grund av fel eller avsikt).

Överväg följande löfte:

 1 iPromiseTo(): Promise<any> {
 2   return new Promise((resolve, reject) => {
 3     if (this.willNeverHappen()) {
 4       resolve('I can forp');                    <====== Resolved
 5     } else if (this.willAlsoNeverHappen()) {
 6       reject('I cannot forp but I can rarp');   <====== Rejected
 7     } else {
 8       console.log('I cannot promise anything'); <====== Unsettled
 9     }
10   });
11 }

Om promise-funktionens kropp slutförs utan att lösa/avvisa (till rad 10 via rad 8) kommer all beroende kod, inklusive then/catch/finally-block som kopplats senare (se nedan), helt enkelt inte att köras och kommer att ignoreras tyst. I de flesta fall anses detta sannolikt vara ett fel, vilket är anledningen till att async-funktioner är fördelaktiga i de flesta situationer. En async-funktion kommer alltid antingen att lösa eller avvisa när dess funktionskropp slutförs (dess funktionskropp kanske aldrig slutförs, men det är ett annat ämne).

Använder detta felaktiga löfte inom Promise-semantik:

 1
 2 youllBeSorry() {
 3   this.iPromiseTo()
 4     .then(promised => {
 5       console.log(promised); //  <=========== NOT REACHED!
 6     }).catch(error => {
 7       console.log(error);    //  <=========== NOT REACHED!
 8     }).finally(() =>
 9       console.log('whew!');  //  <=========== NOT REACHED!
10     });
11 }

Raderna 5, 7 och 9 kommer inte att köras.

Använda det inom async/await-semantik:

 1
 2 async youllBeSorry() {
 3   try {
 4     const promised = await this.iPromiseTo();
 5     ...                    //  <=========== NOT REACHED!
 6   } catch(error) {
 7     console.log(error);    //  <=========== NOT REACHED!
 8   } finally {
 9     console.log('whew!');  //  <=========== NOT REACHED!
10   }
11 }

Raderna 5, 7 och 9 kommer inte att köras och funktionen kommer tyst att returnera på rad 4 utan fel eller någon efterföljande undantagshantering.

En annan klass av löften som aldrig kan slutföras är de med funktionskroppar som aldrig blir klara på grund av en oändlig loop eller oändlig blockering, etc. (på grund av buggar eller avsiktligt). Dessa beter sig i praktiken på samma sätt som de löften som slutför sin funktionskropp men aldrig anropar resolve eller reject, även om det troligen finns andra skillnader mellan de två som inte täcks här.

Okej, så hur kan jag hantera dessa "ouppklarade" löften? Tyvärr finns det inget förskrivet sätt att fånga och hantera detta speciella resultat av löften (i skrivande stund). Det finns vissa råd, men jag har inte provat dem själv: await/async hur man hanterar olösta löften

Det finns top-level await som kanske redan är tillgängligt för dig, eller kommer snart (se också dess äldre IIFE-arbetsrundemotpart): https://v8.dev/features/top-level-await

I idealfallet skulle ECMAScript ge mer övergripande kontroll över asynkront beteende - se Kotlin (eller till och med Python) koroutiner för ett mer robust exempel på hur man hanterar asynkront bättre.

Slutligen (ingen ordlek avsedd), vill jag notera att anledningen till att finally-blocket vanligtvis körs som förväntat i detta specifika fall är att oclif-ramverket används, vilket tyvärr har exit-anrop inbäddade i sina API-rutiner. I alla fall kan den aktuella promisen uppfyllas/avslutas, men den tävlar med exit-anropet i oclif och löses därför inte alltid och beroendekod körs inte alltid innan exit-undantaget kastas och node avslutas. Så det visade sig vara en av de "vanliga misstänkta" i detta specifika fall, men felsökning ledde till upptäckten av dessa brister i ECMAScript.

Svar #2

Förutom de vanliga misstänkta (process.exit(), eller processavslutning/signal, eller krasch/hårdvarufel), finns det några omständigheter där kod i ett finally-block inte kommer att nås?

Om löftet kommer att lösas eller avvisas i framtiden ska det nå det slutliga blocket.


Enligt MDN-dokumentationen,

finally-blocket innehåller satser som ska köras efter att try-blocket och catch-blocket/-blocken har körts, men innan satserna efter try...catch...finally-blocket. Observera att finally-blocket körs oavsett om ett undantag kastas eller inte. Dessutom, om ett undantag kastas, körs satserna i finally-blocket även om inget catch-block hanterar undantaget.

Ett löfte är bara ett JavaScript-objekt. Ett objekt kan ha många tillstånd. Ett löfteobjekt kan vara i pending-tillstånd eller settled-tillstånd. Tillståndet settled kan delas upp i fulfilled och rejected. För detta exempel, föreställ dig bara att vi har endast två tillstånd som PENDING och SETTLED.

Nu, om löftet aldrig löses eller avvisas så kommer det aldrig gå till settled-tillståndet, vilket betyder att ditt then..catch..finally aldrig kommer att anropas. Om inget refererar till löftet kommer det bara att samlas upp av skräpinsamlaren.


I din ursprungliga fråga nämnde du en tredje parts asynkrona metod. Om du tittar på den koden är det första du ser att det finns en uppsättning if(..)-block för att bestämma det aktuella operativsystemet. Men den har inget else-block eller ett standardfall.

Vad händer om ingen av if(..)-blocken triggas? Det finns inget att köra och du har redan returnerat ett löfte med return new Promise(). Så i princip, om ingen av if(..)-blocken triggas, kommer löftet aldrig att ändra sitt tillstånd från pending till settled.

Och sedan, som @Bergi också nämnde, finns det vissa koder som detta. Ett klassiskt Promise-konstruktörsantipattern som han nämnde. Till exempel, se koden nedan,

  isUefiLinux().then(uefi => {
    result.uefi = uefi;
    uuid().then(data => {
      result.serial = data.os;
      if (callback) {
        callback(result);
      }
      resolve(result);
    });
  });

Vad händer om ovanstående isUefiLinux aldrig löses? Återigen kommer then inte att triggas på isUefiLinux och aldrig lösa huvudlöftet.

Nu, om du kollar koden för isUefiLinux så löses det även om det kastar ett fel.

function isUefiLinux() {
  return new Promise((resolve) => {
    process.nextTick(() => {
      fs.stat('/sys/firmware/efi', function (err) {
        //what if this cb never called?
        if (!err) {
          resolve(true);
        } else {
          exec('dmesg | grep -E "EFI v"', function (error, stdout) {
            //what if this cb never called?
            if (!error) {
              const lines = stdout.toString().split('\n');
              resolve(lines.length > 0);
            }
            resolve(false);
          });
        }
      });
    });
  });
}

Men det finns två callback-funktioner i isUefiLinux-metoden, en blandning av löften och callbacks; ett 'helvete'. Men vad händer om dessa callbacks aldrig kallas? Ditt löfte kommer aldrig att lösas.


Jag har verifierat detta inom en IJ-debuggsession - finally-blocket på rad 7 kommer ibland inte att köras och kommer att avslutas direkt på rad 4

Kommer “Occasionally” inte att köras? Förklarar inte ovanstående förklaring detta på någon nivå?

Mer information

  1. https://developer.mozilla.org/sv/docs/Web/JavaScript/Reference/Global_Objects/Promise
  2. https://tc39.es/ecma262/#sec-promise.prototype.finally
  3. https://github.com/domenic/promises-unwrapping/blob/master/docs/states-and-fates.md

Relaterade artiklar