Het debuggen van uitzonderingen in webapplicaties lijkt eenvoudig: pauzeer de uitvoering als er iets misgaat en onderzoek het. Maar het asynchrone karakter van JavaScript maakt dit verrassend complex. Hoe kan Chrome DevTools weten wanneer en waar ze moeten pauzeren wanneer uitzonderingen door beloften en asynchrone functies heen vliegen?
Dit bericht gaat dieper in op de uitdagingen van catch-voorspelling : het vermogen van DevTools om te anticiperen of er later in uw code een uitzondering wordt opgevangen. We zullen onderzoeken waarom het zo lastig is en hoe recente verbeteringen in V8 (de JavaScript-engine die Chrome aandrijft) het nauwkeuriger maken, wat leidt tot een soepelere foutopsporingservaring.
Waarom vangstvoorspelling belangrijk is
In Chrome DevTools heeft u de mogelijkheid om de uitvoering van code alleen te onderbreken voor niet-afgevangen uitzonderingen, waarbij u de wel opgevangen uitzonderingen overslaat.
Achter de schermen stopt de debugger onmiddellijk wanneer er een uitzondering optreedt om de context te behouden. Het is een voorspelling omdat het op dit moment onmogelijk is om zeker te weten of de uitzondering later in de code zal worden opgemerkt of niet, vooral in asynchrone scenario's. Deze onzekerheid komt voort uit de inherente moeilijkheid om programmagedrag te voorspellen, vergelijkbaar met het Halting-probleem .
Neem het volgende voorbeeld: waar moet de debugger pauzeren? (Zoek een antwoord in het volgende gedeelte.)
async function inner() {
throw new Error(); // Should the debugger pause here?
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ... or should the debugger pause here?
}
}
Pauzeren bij uitzonderingen in een debugger kan storend zijn en leiden tot frequente onderbrekingen en sprongen naar onbekende code. Om dit te beperken, kunt u ervoor kiezen om alleen fouten op te sporen die niet zijn onderschept, omdat de kans groter is dat deze daadwerkelijke bugs signaleren. Dit is echter afhankelijk van de nauwkeurigheid van de vangstvoorspelling.
Verkeerde voorspellingen leiden tot frustratie:
- Fout-negatieven (voorspellen dat het niet wordt opgepakt wanneer het wel wordt opgepakt) . Onnodige stops in de debugger.
- Valse positieven (voorspellen dat 'gepakt' wordt terwijl het niet wordt opgepakt) . Gemiste kansen om kritieke fouten op te sporen, waardoor u mogelijk alle uitzonderingen moet debuggen, inclusief de verwachte uitzonderingen.
Een andere methode om onderbrekingen bij het opsporen van fouten te verminderen, is door gebruik te maken van de negeerlijst , die pauzes bij uitzonderingen binnen gespecificeerde code van derden voorkomt. Een nauwkeurige vangstvoorspelling is hier echter nog steeds van cruciaal belang. Als een uitzondering die voortkomt uit code van derden ontsnapt en uw eigen code beïnvloedt, wilt u fouten kunnen opsporen.
Hoe asynchrone code werkt
Beloften, async
en await
en andere asynchrone patronen kunnen leiden tot scenario's waarin een uitzondering of afwijzing, voordat deze wordt afgehandeld, een uitvoeringspad kan volgen dat moeilijk te bepalen is op het moment dat er een uitzondering wordt gegenereerd. Dit komt omdat er mogelijk pas op beloften wordt gewacht of dat catch-handlers worden toegevoegd nadat de uitzondering al heeft plaatsgevonden. Laten we eens kijken naar ons vorige voorbeeld:
async function inner() {
throw new Error();
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ...
}
}
In dit voorbeeld roept outer()
eerst inner()
aan, wat onmiddellijk een uitzondering genereert. Hieruit kan de debugger concluderen dat inner()
een afgewezen belofte zal retourneren, maar momenteel wacht er niets op of handelt deze belofte anderszins af. De debugger kan raden dat outer()
er waarschijnlijk op zal wachten en vermoedt dat het dit in zijn huidige try
-blok zal doen en het daarom zal afhandelen, maar de debugger kan hier pas zeker van zijn nadat de afgewezen belofte is teruggestuurd en de await
instructie uiteindelijk is bereikt.
De debugger kan geen enkele garantie bieden dat vangstvoorspellingen accuraat zullen zijn, maar gebruikt een verscheidenheid aan heuristieken om algemene coderingspatronen correct te voorspellen. Om deze patronen te begrijpen, helpt het om te leren hoe beloften werken.
In V8 wordt een JavaScript- Promise
weergegeven als een object dat zich in een van de volgende drie statussen kan bevinden: vervuld, afgewezen of in behandeling. Als een belofte de status 'vervuld' heeft en u de methode .then()
aanroept, wordt er een nieuwe belofte in behandeling gemaakt en wordt er een nieuwe reactietaak voor de belofte gepland, die de handler uitvoert en vervolgens de belofte op 'vervuld' zet met het resultaat van de handler, of deze op 'afgewezen' zet als de handler een uitzondering genereert. Hetzelfde gebeurt als u de methode .catch()
aanroept voor een afgewezen belofte. Integendeel, het aanroepen van .then()
op een afgewezen belofte of .catch()
op een vervulde belofte zal een belofte in dezelfde staat retourneren en de handler niet uitvoeren.
Een openstaande belofte bevat een reactielijst waarbij elk reactieobject een afhandelingshandler of afwijzingshandler (of beide) en een reactiebelofte bevat. Dus het aanroepen van .then()
op een in behandeling zijnde belofte zal een reactie toevoegen met een vervulde handler, evenals een nieuwe in behandeling zijnde belofte voor de reactiebelofte, die .then()
zal retourneren. Het aanroepen van .catch()
zal een soortgelijke reactie toevoegen, maar met een afwijzingshandler. Het aanroepen .then()
met twee argumenten creëert een reactie met beide handlers, en het aanroepen van .finally()
of het wachten op de belofte zal een reactie toevoegen met twee handlers die ingebouwde functies zijn die specifiek zijn voor het implementeren van deze functies.
Wanneer de openstaande belofte uiteindelijk wordt vervuld of afgewezen, worden reactietaken gepland voor alle vervulde afhandelaars of voor alle afgewezen afhandelaars. De overeenkomstige reactiebeloften zullen dan worden bijgewerkt, waardoor mogelijk hun eigen reactietaken kunnen worden geactiveerd.
Voorbeelden
Beschouw de volgende code:
return new Promise(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Het is misschien niet voor de hand liggend dat deze code drie verschillende Promise
-objecten omvat. De bovenstaande code is gelijk aan de volgende code:
const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;
In dit voorbeeld gebeuren de volgende stappen:
- De
Promise
-constructor wordt aangeroepen. - Er wordt een nieuwe lopende
Promise
gemaakt. - De anonieme functie wordt uitgevoerd.
- Er wordt een uitzondering gegenereerd. Op dit punt moet de debugger beslissen of hij wil stoppen of niet.
- De belofteconstructor vangt deze uitzondering op en wijzigt vervolgens de status van zijn belofte in
rejected
, waarbij de waarde is ingesteld op de fout die is gegenereerd. Het retourneert deze belofte, die is opgeslagen inpromise1
. -
.then()
plant geen reactietaak omdatpromise1
de statusrejected
heeft. In plaats daarvan wordt een nieuwe belofte (promise2
) geretourneerd, die ook de status afgewezen heeft met dezelfde fout. -
.catch()
plant een reactietaak met de opgegeven handler en een nieuwe reactiebelofte die in behandeling is, die wordt geretourneerd alspromise3
. Op dit punt weet de debugger dat de fout zal worden afgehandeld. - Wanneer de reactietaak wordt uitgevoerd, keert de afhandelingsroutine normaal terug en wordt de status van
promise3
gewijzigd infulfilled
.
Het volgende voorbeeld heeft een vergelijkbare structuur, maar de uitvoering is heel anders:
return Promise.resolve()
.then(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Dit komt overeen met:
const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;
In dit voorbeeld gebeuren de volgende stappen:
- Een
Promise
wordt in defulfilled
staat aangemaakt en opgeslagen inpromise1
. - Er wordt een beloftereactietaak gepland met de eerste anonieme functie en de
(pending)
reactiebelofte wordt geretourneerd alspromise2
. - Er wordt een reactie toegevoegd aan
promise2
met een vervulde handler en de bijbehorende reactiebelofte, die wordt geretourneerd alspromise3
. - Er wordt een reactie toegevoegd aan
promise3
met een afgewezen handler en een andere reactiebelofte, die wordt geretourneerd alspromise4
. - De in stap 2 geplande reactietaak wordt uitgevoerd.
- De handler genereert een uitzondering. Op dit punt moet de debugger beslissen of hij wil stoppen of niet. Momenteel is de handler uw enige actieve JavaScript-code.
- Omdat de taak eindigt met een uitzondering, wordt de bijbehorende reactiebelofte (
promise2
) ingesteld op de status afgewezen, waarbij de waarde is ingesteld op de fout die is gegenereerd. - Omdat
promise2
één reactie had, en die reactie geen afgewezen handler had, is de reactiebelofte (promise3
) ook ingesteld oprejected
met dezelfde fout. - Omdat
promise3
één reactie had, en die reactie een afgewezen handler had, wordt er een beloftereactietaak gepland met die handler en zijn reactiebelofte (promise4
). - Wanneer die reactietaak wordt uitgevoerd, keert de handler normaal terug en wordt de status van
promise4
gewijzigd in vervuld.
Methoden voor vangstvoorspelling
Er zijn twee potentiële informatiebronnen voor vangstvoorspellingen. Eén daarvan is de call-stack. Dit is goed voor synchrone uitzonderingen: de debugger kan door de call-stack lopen op dezelfde manier als de code voor het afwikkelen van uitzonderingen en stopt als hij een frame vindt waarin hij zich in een try...catch
-blok bevindt. Voor afgewezen beloften of uitzonderingen in belofteconstructors of in asynchrone functies die nooit zijn opgeschort, vertrouwt de debugger ook op de call-stack, maar in dit geval kan zijn voorspelling niet in alle gevallen betrouwbaar zijn. Dit komt omdat in plaats van een uitzondering naar de dichtstbijzijnde handler te gooien, asynchrone code een afgewezen uitzondering retourneert, en de debugger een paar aannames moet doen over wat de beller ermee zal doen.
Ten eerste gaat de debugger ervan uit dat een functie die een geretourneerde belofte ontvangt, die belofte of een afgeleide belofte waarschijnlijk zal retourneren, zodat asynchrone functies verderop in de stapel de kans krijgen erop te wachten. Ten tweede gaat de debugger ervan uit dat als een belofte wordt teruggestuurd naar een asynchrone functie, deze er snel op zal wachten zonder eerst een try...catch
-blok binnen te gaan of te verlaten. Geen van deze aannames is gegarandeerd correct, maar ze zijn voldoende om de juiste voorspellingen te doen voor de meest voorkomende coderingspatronen met asynchrone functies. In Chrome versie 125 hebben we nog een heuristiek toegevoegd: de debugger controleert of een aangeroepene op het punt staat .catch()
aan te roepen op de waarde die zal worden geretourneerd (of .then()
met twee argumenten, of een reeks aanroepen naar .then()
of .finally()
gevolgd door een .catch()
of een .then()
met twee argumenten). In dit geval gaat de debugger ervan uit dat dit de methoden zijn voor de belofte die we traceren of een methode die daarmee verband houdt, zodat de afwijzing wordt onderschept.
De tweede bron van informatie is de boom van beloftereacties. De debugger begint met een rootbelofte. Soms is dit een belofte waarvoor zojuist de methode reject()
is aangeroepen. Vaker, wanneer er een uitzondering of afwijzing plaatsvindt tijdens een belofte-reactietaak en niets op de call-stack deze lijkt op te vangen, volgt de debugger de belofte die bij de reactie hoort. De debugger kijkt naar alle reacties op de openstaande belofte en ziet of er afwijzingshandlers zijn. Als er reacties zijn die dat niet doen, wordt er gekeken naar de reactiebelofte en wordt er recursief van afgeleid. Als alle reacties uiteindelijk leiden tot een afwijzingsbehandelaar, beschouwt de debugger de afwijzing van de belofte als onderschept. Er zijn enkele speciale gevallen waar rekening mee moet worden gehouden, bijvoorbeeld wanneer de ingebouwde afwijzingshandler voor een .finally()
aanroep niet wordt meegerekend.
De belofte-reactieboom biedt een doorgaans betrouwbare informatiebron als de informatie aanwezig is. In sommige gevallen, zoals bij een aanroep van Promise.reject()
of in een Promise
-constructor of in een asynchrone functie die nog niet op iets heeft gewacht, zijn er geen reacties te traceren en moet de debugger alleen op de call-stack vertrouwen. In andere gevallen bevat de belofte-reactieboom gewoonlijk de handlers die nodig zijn om de vangstvoorspelling af te leiden, maar het is altijd mogelijk dat er later meer handlers worden toegevoegd die de uitzondering van gevangen naar niet-afgevangen zullen veranderen of omgekeerd. Er zijn ook beloften zoals die gemaakt door Promise.all/any/race
, waarbij andere beloften in de groep van invloed kunnen zijn op de manier waarop een afwijzing wordt behandeld. Bij deze methoden gaat de debugger ervan uit dat een afwijzing van een belofte wordt doorgestuurd als de belofte nog in behandeling is.
Kijk eens naar de volgende twee voorbeelden:
Hoewel deze twee voorbeelden van gevangen uitzonderingen op elkaar lijken, vereisen ze heel verschillende heuristieken voor het voorspellen van de vangst. In het eerste voorbeeld wordt een opgeloste belofte gemaakt, vervolgens wordt een reactietaak voor .then()
gepland die een uitzondering genereert, en vervolgens wordt .catch()
aangeroepen om een afwijzingshandler aan de reactiebelofte te koppelen. Wanneer de reactietaak wordt uitgevoerd, wordt de uitzondering gegenereerd en bevat de beloftereactieboom de catch-handler, zodat deze als gevangen wordt gedetecteerd. In het tweede voorbeeld wordt de belofte onmiddellijk afgewezen voordat de code voor het toevoegen van een catch-handler wordt uitgevoerd. Er zijn dus geen afwijzingshandlers in de reactieboom van de belofte. De debugger moet naar de call-stack kijken, maar er zijn ook geen try...catch
-blokken. Om dit correct te voorspellen, scant de debugger vooruit naar de huidige locatie in de code om de aanroep van .catch()
te vinden, en gaat er op basis daarvan van uit dat de afwijzing uiteindelijk zal worden afgehandeld.
Samenvatting
Hopelijk heeft deze uitleg licht geworpen op hoe vangstvoorspelling werkt in Chrome DevTools, en op de sterke punten en beperkingen ervan. Als u foutopsporingsproblemen ondervindt als gevolg van onjuiste voorspellingen, kunt u deze opties overwegen:
- Verander het coderingspatroon in iets dat eenvoudiger te voorspellen is, zoals het gebruik van asynchrone functies.
- Selecteer deze optie om alle uitzonderingen te beëindigen als DevTools er niet in slaagt te stoppen wanneer dat zou moeten.
- Gebruik een breekpunt 'Hier nooit pauzeren' of een voorwaardelijk breekpunt als de debugger ergens stopt waar u niet wilt.
Dankbetuigingen
Onze diepste dank gaat uit naar Sofia Emelianova en Jecelyn Yeen voor hun onschatbare hulp bij het bewerken van dit bericht!
,Het debuggen van uitzonderingen in webapplicaties lijkt eenvoudig: pauzeer de uitvoering als er iets misgaat en onderzoek het. Maar het asynchrone karakter van JavaScript maakt dit verrassend complex. Hoe kan Chrome DevTools weten wanneer en waar ze moeten pauzeren wanneer uitzonderingen door beloften en asynchrone functies heen vliegen?
Dit bericht gaat dieper in op de uitdagingen van catch-voorspelling : het vermogen van DevTools om te anticiperen of er later in uw code een uitzondering wordt opgevangen. We zullen onderzoeken waarom het zo lastig is en hoe recente verbeteringen in V8 (de JavaScript-engine die Chrome aandrijft) het nauwkeuriger maken, wat leidt tot een soepelere foutopsporingservaring.
Waarom vangstvoorspelling belangrijk is
In Chrome DevTools heeft u de mogelijkheid om de uitvoering van code alleen te onderbreken voor niet-afgevangen uitzonderingen, waarbij u de wel opgevangen uitzonderingen overslaat.
Achter de schermen stopt de debugger onmiddellijk wanneer er een uitzondering optreedt om de context te behouden. Het is een voorspelling omdat het op dit moment onmogelijk is om zeker te weten of de uitzondering later in de code zal worden opgemerkt of niet, vooral in asynchrone scenario's. Deze onzekerheid komt voort uit de inherente moeilijkheid om programmagedrag te voorspellen, vergelijkbaar met het Halting-probleem .
Neem het volgende voorbeeld: waar moet de debugger pauzeren? (Zoek een antwoord in het volgende gedeelte.)
async function inner() {
throw new Error(); // Should the debugger pause here?
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ... or should the debugger pause here?
}
}
Pauzeren bij uitzonderingen in een debugger kan storend zijn en leiden tot frequente onderbrekingen en sprongen naar onbekende code. Om dit te beperken, kunt u ervoor kiezen om alleen fouten op te sporen die niet zijn onderschept, omdat de kans groter is dat deze daadwerkelijke bugs signaleren. Dit is echter afhankelijk van de nauwkeurigheid van de vangstvoorspelling.
Verkeerde voorspellingen leiden tot frustratie:
- Fout-negatieven (voorspellen dat het niet wordt opgepakt wanneer het wel wordt opgepakt) . Onnodige stops in de debugger.
- Valse positieven (voorspellen dat 'gepakt' wordt terwijl het niet wordt opgepakt) . Gemiste kansen om kritieke fouten op te sporen, waardoor u mogelijk alle uitzonderingen moet debuggen, inclusief de verwachte uitzonderingen.
Een andere methode om onderbrekingen bij het opsporen van fouten te verminderen, is door gebruik te maken van de negeerlijst , die pauzes bij uitzonderingen binnen gespecificeerde code van derden voorkomt. Een nauwkeurige vangstvoorspelling is hier echter nog steeds van cruciaal belang. Als een uitzondering die voortkomt uit code van derden ontsnapt en uw eigen code beïnvloedt, wilt u fouten kunnen opsporen.
Hoe asynchrone code werkt
Beloften, async
en await
en andere asynchrone patronen kunnen leiden tot scenario's waarin een uitzondering of afwijzing, voordat deze wordt afgehandeld, een uitvoeringspad kan volgen dat moeilijk te bepalen is op het moment dat er een uitzondering wordt gegenereerd. Dit komt omdat er mogelijk pas op beloften wordt gewacht of dat catch-handlers worden toegevoegd nadat de uitzondering al heeft plaatsgevonden. Laten we eens kijken naar ons vorige voorbeeld:
async function inner() {
throw new Error();
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ...
}
}
In dit voorbeeld roept outer()
eerst inner()
aan, wat onmiddellijk een uitzondering genereert. Hieruit kan de debugger concluderen dat inner()
een afgewezen belofte zal retourneren, maar momenteel wacht er niets op of handelt deze belofte anderszins af. De debugger kan raden dat outer()
er waarschijnlijk op zal wachten en vermoedt dat het dit in zijn huidige try
-blok zal doen en het daarom zal afhandelen, maar de debugger kan hier pas zeker van zijn nadat de afgewezen belofte is teruggestuurd en de await
instructie uiteindelijk is bereikt.
De debugger kan geen enkele garantie bieden dat vangstvoorspellingen accuraat zijn, maar gebruikt een verscheidenheid aan heuristieken om algemene coderingspatronen correct te voorspellen. Om deze patronen te begrijpen, helpt het om te leren hoe beloften werken.
In V8 wordt een JavaScript- Promise
weergegeven als een object dat zich in een van de volgende drie statussen kan bevinden: vervuld, afgewezen of in behandeling. Als een belofte de status 'vervuld' heeft en u de methode .then()
aanroept, wordt er een nieuwe belofte in behandeling gemaakt en wordt er een nieuwe reactietaak voor de belofte gepland, die de handler uitvoert en vervolgens de belofte op 'vervuld' zet met het resultaat van de handler, of deze op 'afgewezen' zet als de handler een uitzondering genereert. Hetzelfde gebeurt als u de methode .catch()
aanroept voor een afgewezen belofte. Integendeel, het aanroepen van .then()
op een afgewezen belofte of .catch()
op een vervulde belofte zal een belofte in dezelfde staat retourneren en de handler niet uitvoeren.
Een openstaande belofte bevat een reactielijst waarbij elk reactieobject een afhandelingshandler of afwijzingshandler (of beide) en een reactiebelofte bevat. Dus het aanroepen van .then()
op een in behandeling zijnde belofte zal een reactie toevoegen met een vervulde handler, evenals een nieuwe in behandeling zijnde belofte voor de reactiebelofte, die .then()
zal retourneren. Het aanroepen van .catch()
zal een soortgelijke reactie toevoegen, maar met een afwijzingshandler. Het aanroepen .then()
met twee argumenten creëert een reactie met beide handlers, en het aanroepen van .finally()
of het wachten op de belofte zal een reactie toevoegen met twee handlers die ingebouwde functies zijn die specifiek zijn voor het implementeren van deze functies.
Wanneer de openstaande belofte uiteindelijk wordt vervuld of afgewezen, worden reactietaken gepland voor alle vervulde afhandelaars of voor alle afgewezen afhandelaars. De overeenkomstige reactiebeloften zullen dan worden bijgewerkt, waardoor mogelijk hun eigen reactietaken kunnen worden geactiveerd.
Voorbeelden
Beschouw de volgende code:
return new Promise(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Het is misschien niet voor de hand liggend dat deze code drie verschillende Promise
-objecten omvat. De bovenstaande code is gelijk aan de volgende code:
const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;
In dit voorbeeld gebeuren de volgende stappen:
- De
Promise
-constructor wordt aangeroepen. - Er wordt een nieuwe lopende
Promise
gemaakt. - De anonieme functie wordt uitgevoerd.
- Er wordt een uitzondering gegenereerd. Op dit punt moet de debugger beslissen of hij wil stoppen of niet.
- De belofteconstructor vangt deze uitzondering op en wijzigt vervolgens de status van zijn belofte in
rejected
, waarbij de waarde is ingesteld op de fout die is gegenereerd. Het retourneert deze belofte, die is opgeslagen inpromise1
. -
.then()
plant geen reactietaak omdatpromise1
de statusrejected
heeft. In plaats daarvan wordt een nieuwe belofte (promise2
) geretourneerd, die ook de status afgewezen heeft met dezelfde fout. -
.catch()
plant een reactietaak met de opgegeven handler en een nieuwe reactiebelofte die in behandeling is, die wordt geretourneerd alspromise3
. Op dit punt weet de debugger dat de fout zal worden afgehandeld. - Wanneer de reactietaak wordt uitgevoerd, keert de afhandelingsroutine normaal terug en wordt de status van
promise3
gewijzigd infulfilled
.
Het volgende voorbeeld heeft een vergelijkbare structuur, maar de uitvoering is heel anders:
return Promise.resolve()
.then(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Dit komt overeen met:
const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;
In dit voorbeeld gebeuren de volgende stappen:
- Een
Promise
wordt in defulfilled
staat aangemaakt en opgeslagen inpromise1
. - Er wordt een beloftereactietaak gepland met de eerste anonieme functie en de
(pending)
reactiebelofte wordt geretourneerd alspromise2
. - Er wordt een reactie toegevoegd aan
promise2
met een vervulde handler en de bijbehorende reactiebelofte, die wordt geretourneerd alspromise3
. - Er wordt een reactie toegevoegd aan
promise3
met een afgewezen handler en een andere reactiebelofte, die wordt geretourneerd alspromise4
. - De in stap 2 geplande reactietaak wordt uitgevoerd.
- De handler genereert een uitzondering. Op dit punt moet de debugger beslissen of hij wil stoppen of niet. Momenteel is de handler uw enige actieve JavaScript-code.
- Omdat de taak eindigt met een uitzondering, wordt de bijbehorende reactiebelofte (
promise2
) ingesteld op de status afgewezen, waarbij de waarde wordt ingesteld op de fout die is gegenereerd. - Omdat
promise2
één reactie had, en die reactie geen afgewezen handler had, is de reactiebelofte (promise3
) ook ingesteld oprejected
met dezelfde fout. - Omdat
promise3
één reactie had, en die reactie een afgewezen handler had, wordt er een beloftereactietaak gepland met die handler en zijn reactiebelofte (promise4
). - Wanneer die reactietaak wordt uitgevoerd, keert de handler normaal terug en wordt de status van
promise4
gewijzigd in vervuld.
Methoden voor vangstvoorspelling
Er zijn twee potentiële informatiebronnen voor vangstvoorspellingen. Eén daarvan is de call-stack. Dit is goed voor synchrone uitzonderingen: de debugger kan door de call-stack lopen op dezelfde manier als de code voor het afwikkelen van uitzonderingen en stopt als hij een frame vindt waarin hij zich in een try...catch
-blok bevindt. Voor afgewezen beloften of uitzonderingen in belofteconstructors of in asynchrone functies die nooit zijn opgeschort, vertrouwt de debugger ook op de call-stack, maar in dit geval kan zijn voorspelling niet in alle gevallen betrouwbaar zijn. Dit komt omdat in plaats van een uitzondering naar de dichtstbijzijnde handler te gooien, asynchrone code een afgewezen uitzondering retourneert, en de debugger een paar aannames moet doen over wat de beller ermee zal doen.
Ten eerste gaat de debugger ervan uit dat een functie die een geretourneerde belofte ontvangt, die belofte of een afgeleide belofte waarschijnlijk zal retourneren, zodat asynchrone functies verderop in de stapel de kans krijgen erop te wachten. Ten tweede gaat de debugger ervan uit dat als een belofte wordt teruggestuurd naar een asynchrone functie, deze er snel op zal wachten zonder eerst een try...catch
-blok binnen te gaan of te verlaten. Geen van deze aannames is gegarandeerd correct, maar ze zijn voldoende om de juiste voorspellingen te doen voor de meest voorkomende coderingspatronen met asynchrone functies. In Chrome versie 125 hebben we nog een heuristiek toegevoegd: de debugger controleert of een aangeroepene op het punt staat .catch()
aan te roepen op de waarde die zal worden geretourneerd (of .then()
met twee argumenten, of een reeks aanroepen naar .then()
of .finally()
gevolgd door een .catch()
of een .then()
met twee argumenten). In dit geval gaat de debugger ervan uit dat dit de methoden zijn voor de belofte die we traceren of een methode die daarmee verband houdt, zodat de afwijzing wordt onderschept.
De tweede bron van informatie is de boom van beloftereacties. De debugger begint met een rootbelofte. Soms is dit een belofte waarvoor zojuist de methode reject()
is aangeroepen. Vaker, wanneer er een uitzondering of afwijzing plaatsvindt tijdens een belofte-reactietaak, en niets op de call-stack deze lijkt op te vangen, volgt de debugger de belofte die bij de reactie hoort. De debugger kijkt naar alle reacties op de openstaande belofte en ziet of er afwijzingshandlers zijn. Als er reacties zijn die dat niet doen, wordt er gekeken naar de reactiebelofte en wordt er recursief van afgeleid. Als alle reacties uiteindelijk leiden tot een afwijzingsbehandelaar, beschouwt de debugger de afwijzing van de belofte als onderschept. Er zijn enkele speciale gevallen waar rekening mee moet worden gehouden, bijvoorbeeld wanneer de ingebouwde afwijzingshandler voor een .finally()
aanroep niet wordt meegerekend.
De belofte-reactieboom biedt een doorgaans betrouwbare informatiebron als de informatie aanwezig is. In sommige gevallen, zoals bij een aanroep van Promise.reject()
of in een Promise
-constructor of in een asynchrone functie die nog niet op iets heeft gewacht, zijn er geen reacties te traceren en moet de debugger alleen op de call-stack vertrouwen. In andere gevallen bevat de belofte-reactieboom gewoonlijk de handlers die nodig zijn om de vangstvoorspelling af te leiden, maar het is altijd mogelijk dat er later meer handlers worden toegevoegd die de uitzondering van gevangen naar niet-afgevangen zullen veranderen of omgekeerd. Er zijn ook beloften zoals die gemaakt door Promise.all/any/race
, waarbij andere beloften in de groep van invloed kunnen zijn op de manier waarop een afwijzing wordt behandeld. Bij deze methoden gaat de debugger ervan uit dat een afwijzing van een belofte wordt doorgestuurd als de belofte nog in behandeling is.
Kijk eens naar de volgende twee voorbeelden:
Hoewel deze twee voorbeelden van gevangen uitzonderingen op elkaar lijken, vereisen ze heel verschillende heuristieken voor het voorspellen van de vangst. In het eerste voorbeeld wordt een opgeloste belofte gemaakt, vervolgens wordt een reactietaak voor .then()
gepland die een uitzondering genereert, en vervolgens wordt .catch()
aangeroepen om een afwijzingshandler aan de reactiebelofte te koppelen. Wanneer de reactietaak wordt uitgevoerd, wordt de uitzondering gegenereerd en bevat de belofte-reactieboom de catch-handler, zodat deze als gevangen wordt gedetecteerd. In het tweede voorbeeld wordt de belofte onmiddellijk afgewezen voordat de code voor het toevoegen van een catch-handler wordt uitgevoerd. Er zijn dus geen afwijzingshandlers in de reactieboom van de belofte. De debugger moet naar de call-stack kijken, maar er zijn ook geen try...catch
-blokken. Om dit correct te voorspellen, scant de debugger vóór de huidige locatie in de code om de aanroep van .catch()
te vinden, en gaat er op basis daarvan van uit dat de afwijzing uiteindelijk zal worden afgehandeld.
Samenvatting
Hopelijk heeft deze uitleg licht geworpen op hoe vangstvoorspelling werkt in Chrome DevTools, en op de sterke punten en beperkingen ervan. Als u foutopsporingsproblemen ondervindt als gevolg van onjuiste voorspellingen, kunt u deze opties overwegen:
- Verander het coderingspatroon in iets dat eenvoudiger te voorspellen is, zoals het gebruik van asynchrone functies.
- Selecteer deze optie om alle uitzonderingen te beëindigen als DevTools er niet in slaagt te stoppen wanneer dat zou moeten.
- Gebruik een breekpunt 'Hier nooit pauzeren' of een voorwaardelijk breekpunt als de debugger ergens stopt waar u niet wilt.
Dankbetuigingen
Onze diepste dank gaat uit naar Sofia Emelianova en Jecelyn Yeen voor hun onschatbare hulp bij het bewerken van dit bericht!
,Het debuggen van uitzonderingen in webapplicaties lijkt eenvoudig: pauzeer de uitvoering als er iets misgaat en onderzoek het. Maar het asynchrone karakter van JavaScript maakt dit verrassend complex. Hoe kan Chrome DevTools weten wanneer en waar ze moeten pauzeren wanneer uitzonderingen door beloften en asynchrone functies heen vliegen?
Dit bericht gaat dieper in op de uitdagingen van catch-voorspelling : het vermogen van DevTools om te anticiperen of er later in uw code een uitzondering wordt opgevangen. We zullen onderzoeken waarom het zo lastig is en hoe recente verbeteringen in V8 (de JavaScript-engine die Chrome aandrijft) het nauwkeuriger maken, wat leidt tot een soepelere foutopsporingservaring.
Waarom vangstvoorspelling belangrijk is
In Chrome DevTools heeft u de mogelijkheid om de uitvoering van code alleen te onderbreken voor niet-afgevangen uitzonderingen, waarbij u de wel opgevangen uitzonderingen overslaat.
Achter de schermen stopt de debugger onmiddellijk wanneer er een uitzondering optreedt om de context te behouden. Het is een voorspelling omdat het op dit moment onmogelijk is om zeker te weten of de uitzondering later in de code zal worden opgemerkt of niet, vooral in asynchrone scenario's. Deze onzekerheid komt voort uit de inherente moeilijkheid om programmagedrag te voorspellen, vergelijkbaar met het Halting-probleem .
Neem het volgende voorbeeld: waar moet de debugger pauzeren? (Zoek een antwoord in het volgende gedeelte.)
async function inner() {
throw new Error(); // Should the debugger pause here?
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ... or should the debugger pause here?
}
}
Pauzeren bij uitzonderingen in een debugger kan storend zijn en leiden tot frequente onderbrekingen en sprongen naar onbekende code. Om dit te beperken, kunt u ervoor kiezen om alleen fouten op te sporen die niet zijn onderschept, omdat de kans groter is dat deze daadwerkelijke bugs signaleren. Dit is echter afhankelijk van de nauwkeurigheid van de vangstvoorspelling.
Verkeerde voorspellingen leiden tot frustratie:
- Fout-negatieven (voorspellen dat het niet wordt opgepakt wanneer het wel wordt opgepakt) . Onnodige stops in de debugger.
- Valse positieven (voorspellen dat 'gepakt' wordt terwijl het niet wordt opgepakt) . Gemiste kansen om kritieke fouten op te sporen, waardoor u mogelijk alle uitzonderingen moet debuggen, inclusief de verwachte uitzonderingen.
Een andere methode om onderbrekingen bij het opsporen van fouten te verminderen, is door gebruik te maken van de negeerlijst , die pauzes bij uitzonderingen binnen gespecificeerde code van derden voorkomt. Een nauwkeurige vangstvoorspelling is hier echter nog steeds van cruciaal belang. Als een uitzondering die voortkomt uit code van derden ontsnapt en uw eigen code beïnvloedt, wilt u fouten kunnen opsporen.
Hoe asynchrone code werkt
Beloften, async
en await
en andere asynchrone patronen kunnen leiden tot scenario's waarin een uitzondering of afwijzing, voordat deze wordt afgehandeld, een uitvoeringspad kan volgen dat moeilijk te bepalen is op het moment dat er een uitzondering wordt gegenereerd. Dit komt omdat er mogelijk pas op beloften wordt gewacht of dat catch-handlers worden toegevoegd nadat de uitzondering al heeft plaatsgevonden. Laten we eens kijken naar ons vorige voorbeeld:
async function inner() {
throw new Error();
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ...
}
}
In dit voorbeeld roept outer()
eerst inner()
aan, wat onmiddellijk een uitzondering genereert. Hieruit kan de debugger concluderen dat inner()
een afgewezen belofte zal retourneren, maar momenteel wacht er niets op of handelt deze belofte anderszins af. De debugger kan raden dat outer()
er waarschijnlijk op zal wachten en vermoedt dat het dit in zijn huidige try
-blok zal doen en het daarom zal afhandelen, maar de debugger kan hier pas zeker van zijn nadat de afgewezen belofte is teruggestuurd en de await
instructie uiteindelijk is bereikt.
De debugger kan geen enkele garantie bieden dat vangstvoorspellingen accuraat zullen zijn, maar gebruikt een verscheidenheid aan heuristieken om algemene coderingspatronen correct te voorspellen. Om deze patronen te begrijpen, helpt het om te leren hoe beloften werken.
In V8 wordt een JavaScript- Promise
weergegeven als een object dat zich in een van de volgende drie statussen kan bevinden: vervuld, afgewezen of in behandeling. Als een belofte de status 'vervuld' heeft en u de methode .then()
aanroept, wordt er een nieuwe belofte in behandeling gemaakt en wordt er een nieuwe reactietaak voor de belofte gepland, die de handler uitvoert en vervolgens de belofte op 'vervuld' zet met het resultaat van de handler, of deze op 'afgewezen' zet als de handler een uitzondering genereert. Hetzelfde gebeurt als u de methode .catch()
aanroept voor een afgewezen belofte. Integendeel, het aanroepen van .then()
op een afgewezen belofte of .catch()
op een vervulde belofte zal een belofte in dezelfde staat retourneren en de handler niet uitvoeren.
Een openstaande belofte bevat een reactielijst waarbij elk reactieobject een afhandelingshandler of afwijzingshandler (of beide) en een reactiebelofte bevat. Dus het aanroepen van .then()
op een in behandeling zijnde belofte zal een reactie toevoegen met een vervulde handler, evenals een nieuwe in behandeling zijnde belofte voor de reactiebelofte, die .then()
zal retourneren. Het aanroepen van .catch()
zal een soortgelijke reactie toevoegen, maar met een afwijzingshandler. Het aanroepen .then()
met twee argumenten creëert een reactie met beide handlers, en het aanroepen van .finally()
of het wachten op de belofte zal een reactie toevoegen met twee handlers die ingebouwde functies zijn die specifiek zijn voor het implementeren van deze functies.
Wanneer de openstaande belofte uiteindelijk wordt vervuld of afgewezen, worden reactietaken gepland voor alle vervulde afhandelaars of voor alle afgewezen afhandelaars. De overeenkomstige reactiebeloften zullen dan worden bijgewerkt, waardoor mogelijk hun eigen reactietaken kunnen worden geactiveerd.
Voorbeelden
Beschouw de volgende code:
return new Promise(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Het is misschien niet voor de hand liggend dat deze code drie verschillende Promise
-objecten omvat. De bovenstaande code is gelijk aan de volgende code:
const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;
In dit voorbeeld gebeuren de volgende stappen:
- De
Promise
-constructor wordt aangeroepen. - Er wordt een nieuwe lopende
Promise
gemaakt. - De anonieme functie wordt uitgevoerd.
- Er wordt een uitzondering gegenereerd. Op dit punt moet de debugger beslissen of hij wil stoppen of niet.
- De belofteconstructor vangt deze uitzondering op en wijzigt vervolgens de status van zijn belofte in
rejected
, waarbij de waarde is ingesteld op de fout die is gegenereerd. Het retourneert deze belofte, die is opgeslagen inpromise1
. -
.then()
plant geen reactietaak omdatpromise1
de statusrejected
heeft. In plaats daarvan wordt een nieuwe belofte (promise2
) geretourneerd, die ook de status afgewezen heeft met dezelfde fout. -
.catch()
plant een reactietaak met de verstrekte handler en een nieuwe belofte in afwachting van de reacties, die wordt teruggegeven alspromise3
. Op dit punt weet de foutopsporing dat de fout zal worden afgehandeld. - Wanneer de reactietaak loopt, keert de handler normaal terug en wordt de staat van
promise3
gewijzigd om tefulfilled
.
Het volgende voorbeeld heeft een vergelijkbare structuur, maar de uitvoering is heel anders:
return Promise.resolve()
.then(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Dit is gelijk aan:
const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;
In dit voorbeeld gebeuren de volgende stappen:
- Een
Promise
wordt gecreëerd in defulfilled
staat en opgeslagen inpromise1
. - Een belofte -reactietaak is gepland met de eerste anonieme functie en de
(pending)
reactiebelofte wordt geretourneerd alspromise2
. - Een reactie wordt toegevoegd aan
promise2
met een vervulde handler en zijn reactiebelofte, die wordt teruggegeven alspromise3
. - Een reactie wordt toegevoegd aan
promise3
met een afgewezen handler en een andere reactiebelofte, die wordt geretourneerd alspromise4
. - De reactietaak gepland in stap 2 wordt uitgevoerd.
- De handler gooit een uitzondering. Op dit punt moet de debugger beslissen of ze moeten stoppen of niet. Momenteel is de handler uw enige lopende JavaScript -code.
- Omdat de taak eindigt met een uitzondering, is de bijbehorende reactiebelofte (
promise2
) ingesteld op de afgewezen status met zijn waarde ingesteld op de fout die is gegooid. - Omdat
promise2
één reactie had en die reactie geen afgewezen handler had, is de reactiebelofte (promise3
) ook ingesteld om met dezelfde foutrejected
. - Omdat
promise3
één reactie had en die reactie een afgewezen handler had, is een belofte -reactietaak gepland met die handler en zijn reactiebelofte (promise4
). - Wanneer die reactietaak loopt, keert de handler normaal terug en wordt de staat van
promise4
gewijzigd om te vervuld.
Methoden voor het vangen
Er zijn twee potentiële informatiebronnen voor vangstvoorspelling. Een daarvan is de oproepstapel. Dit is gezond voor synchrone uitzonderingen: de debugger kan de call -stack op dezelfde manier lopen als de uitzondering die de afgewikkelingscode zal ontspannen en stopt als het een frame vindt waar het in een try...catch
vangstblok. Voor afgewezen beloften of uitzonderingen in belofteconstructors of in asynchrone functies die nog nooit zijn opgeschort, vertrouwt de debugger ook op de call -stack, maar in dit geval kan de voorspelling in dit geval in alle gevallen niet betrouwbaar zijn. Dit komt omdat in plaats van een uitzondering op de dichtstbijzijnde handler te gooien, asynchrone code een afgewezen uitzondering zal retourneren, en de debugger een paar veronderstellingen moet doen over wat de beller ermee zal doen.
Ten eerste veronderstelt de debugger dat een functie die een geretourneerde belofte ontvangt waarschijnlijk die belofte of een afgeleide belofte zal retourneren, zodat asynchrone functies verder de stapel verderop zullen wachten. Ten tweede gaat de debugger ervan uit dat als een belofte wordt teruggestuurd naar een asynchrone functie, deze deze binnenkort zal wachten zonder eerst binnen te komen of te try...catch
blok. Geen van deze veronderstellingen is gegarandeerd correct, maar ze zijn voldoende om de juiste voorspellingen te doen voor de meest voorkomende coderingspatronen met asynchrone functies. In Chrome-versie 125 hebben we een andere heuristiek toegevoegd: de debugger controleert of een Callee op het punt staat .catch()
te bellen over de waarde die wordt geretourneerd (of .then()
met twee argumenten, of een keten van oproepen aan .then()
of .finally()
gevolgd door een .catch()
of een twee-argument .then()
). In dit geval gaat de debugger ervan uit dat dit de methoden zijn over de belofte die we traceren of een die hiermee gerelateerd zijn, dus de afwijzing zal worden gevangen.
De tweede informatiebron is de boom van beloftereacties. De debugger begint met een wortelbelofte. Soms is dit een belofte waarvoor de methode zijn reject()
zojuist is opgeroepen. Vaker, wanneer een uitzondering of afwijzing plaatsvindt tijdens een belofte -reactietaak en niets op de call -stapel het lijkt te vangen, volgt de debugger van de belofte die verband houdt met de reactie. De debugger kijkt naar alle reacties op de hangende belofte en ziet of ze afwijzers hebben. Als er geen reacties zijn, kijkt het naar de reactiebelofte en spreekt er recursief van. Als alle reacties uiteindelijk leiden tot een afwijzingshandler, beschouwt de debugger de belofte -afwijzing als gevangen. Er zijn enkele speciale gevallen om bijvoorbeeld de ingebouwde afwijzingshandler te dekken voor een .finally()
Call.
De belofte -reactieboom biedt een meestal betrouwbare informatiebron als de informatie er is. In sommige gevallen, zoals een oproep om Promise
Promise.reject()
In andere gevallen bevat de belofte -reactieboom meestal de handlers die nodig zijn om vangst voorspelling af te leiden, maar het is altijd mogelijk dat later meer handlers zullen worden toegevoegd die de uitzondering zullen veranderen van gevangen tot niet -ongunst of vice versa. Er zijn ook beloften zoals die gecreëerd door Promise.all/any/race
, waar andere beloften in de groep kunnen beïnvloeden hoe een afwijzing wordt behandeld. Voor deze methoden veronderstelt de debugger dat een belofteafwijzing zal worden doorgestuurd als de belofte nog in behandeling is.
Bekijk de volgende twee voorbeelden:
Hoewel deze twee voorbeelden van gevangen uitzonderingen er vergelijkbaar uitzien, vereisen ze een heel andere heuristieken van de vangstvoorspelling. In het eerste voorbeeld wordt een opgeloste belofte gemaakt, vervolgens is er een reactietaak voor .then()
gepland die een uitzondering zal geven, dan wordt .catch()
aangeroepen om een afwijzingshandler aan de reactiebelofte te bevestigen. Wanneer de reactietaak wordt uitgevoerd, wordt de uitzondering weggegooid en zal de belofte -reactieboom de vangsthandler bevatten, zodat deze wordt gedetecteerd als gevangen. In het tweede voorbeeld wordt de belofte onmiddellijk afgewezen voordat de code om een vangsthandler toe te voegen wordt uitgevoerd, dus er zijn geen afwijzingshandlers in de reactieboom van de belofte. De debugger moet naar de call -stack kijken, maar er zijn ook geen try...catch
blokken. Om dit correct te voorspellen, scant de debugger voor de huidige locatie in de code om de oproep naar .catch()
te vinden en neemt op basis van die basis aan dat de afwijzing uiteindelijk zal worden afgehandeld.
Samenvatting
Hopelijk heeft deze verklaring licht geworpen op hoe vangstvoorspelling werkt in Chrome Devtools, zijn sterke punten en zijn beperkingen. Als u foutopsporingskwesties tegenkomt vanwege onjuiste voorspellingen, overweeg dan deze opties:
- Verander het coderingspatroon in iets eenvoudiger om te voorspellen, zoals het gebruik van async -functies.
- Selecteer om op alle uitzonderingen te breken als Devtools niet stopt wanneer het zou moeten.
- Gebruik een "nooit pauze hier" breekpunt of voorwaardelijk breekpunt als de debugger stopt ergens waar je niet wilt.
Dankbetuigingen
Onze diepste dankbaarheid gaat uit naar Sofia Emelianova en Jecelyn Yeen voor hun onschatbare hulp bij het bewerken van dit bericht!
,Debuggen uitzonderingen op webtoepassingen lijken eenvoudig: pauzeer de uitvoering wanneer er iets misgaat en onderzoekt. Maar het asynchrone karakter van JavaScript maakt dit verrassend complex. Hoe kunnen Chrome Devtools weten wanneer en waar te pauzeren wanneer uitzonderingen door beloften en asynchrone functies vliegen?
Dit bericht duikt in de uitdagingen van vangstvoorspelling - het vermogen van Devtools om te anticiperen of een uitzondering later in uw code zal worden gevangen. We zullen onderzoeken waarom het zo lastig is en hoe recente verbeteringen in V8 (de JavaScript Engine Powering Chrome) het nauwkeuriger maken, wat leidt tot een soepelere foutopsporingservaring.
Waarom voorspelling zijn van belang
In Chrome Devtools heb je een optie om code -uitvoering alleen te pauzeren voor niet -uitzonderingen, die worden overgeslagen die worden gevangen.
Achter de schermen stopt de debugger onmiddellijk wanneer zich een uitzondering voordoet om de context te behouden. Het is een voorspelling omdat het op dit moment onmogelijk is om zeker te weten of de uitzondering later in de code zal worden gevangen, vooral in asynchrone scenario's. Deze onzekerheid komt voort uit de inherente moeilijkheid om programmagedrag te voorspellen, vergelijkbaar met het stopprobleem .
Overweeg het volgende voorbeeld: waar moet de debugger pauzeren? (Zoek naar een antwoord in de volgende sectie.)
async function inner() {
throw new Error(); // Should the debugger pause here?
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ... or should the debugger pause here?
}
}
Pauzeren op uitzonderingen in een debugger kan storend zijn en leiden tot frequente onderbrekingen en springt naar onbekende code. Om dit te verzachten, kunt u ervoor kiezen om alleen niet -ongunstige uitzonderingen te debuggen, die vaker daadwerkelijke bugs signaleren. Dit is echter gebaseerd op de nauwkeurigheid van vangstvoorspelling.
Onjuiste voorspellingen leiden tot frustratie:
- Valse negatieven (voorspellend "ongunst" wanneer het wordt gepakt) . Onnodige stops in de foutopsporing.
- Valse positieven (voorspellend "gevangen" wanneer het niet zal worden ontslagen) . Gemiste kansen om kritische fouten te vangen, waardoor u mogelijk wordt gedwongen om alle uitzonderingen op te lossen, inclusief verwachte.
Een andere methode om foutopsporingsonderbrekingen te verminderen, is met behulp van de lijst met negeren , die voorkomt dat pauzes op uitzonderingen binnen de opgegeven code van derden worden voorkomt. Nauwkeurige vangstvoorspelling is hier echter nog steeds cruciaal. Als een uitzondering die afkomstig is van code van derden ontsnapt en uw eigen code beïnvloedt, wilt u deze kunnen debuggen.
Hoe asynchrone code werkt
Beloften, async
en await
, en andere asynchrone patronen kunnen leiden tot scenario's waarin een uitzondering of afwijzing, voordat ze worden behandeld, een uitvoeringspad kunnen nemen dat moeilijk te bepalen is op het moment dat een uitzondering wordt gegooid. Dit komt omdat beloften mogelijk niet worden verwacht of vangst handlers hebben toegevoegd totdat de uitzondering al is opgetreden. Laten we eens kijken naar ons vorige voorbeeld:
async function inner() {
throw new Error();
}
async function outer() {
try {
const promise = inner();
// ...
await promise;
} catch (e) {
// ...
}
}
In dit voorbeeld roept outer()
eerst inner()
aan die onmiddellijk een uitzondering gooit. Hieruit kan de debugger concluderen dat inner()
een afgewezen belofte zal retourneren, maar momenteel wacht niets op of omgaan met die belofte. De debugger kan raden dat outer()
het waarschijnlijk zal wachten en raden dat het dit zal doen in zijn huidige try
-blok en daarom zal het afhandelen, maar de debugger kan hier niet zeker van zijn totdat nadat de afgewezen belofte is teruggestuurd en de await
uiteindelijk wordt bereikt.
De debugger kan geen garanties bieden dat vangstvoorspellingen nauwkeurig zullen zijn, maar het gebruikt een verscheidenheid aan heuristieken voor gemeenschappelijke coderingspatronen om correct te voorspellen. Om deze patronen te begrijpen, helpt het om te leren hoe beloften werken.
In V8 wordt een JavaScript Promise
weergegeven als een object dat in een van de drie staten kan zijn: vervuld, afgewezen of in behandeling. Als een belofte in de vervulde staat is en u de .then()
roept, wordt een nieuwe hangende belofte gemaakt en wordt een nieuwe belofte -reactietaak gepland die de handler zal uitvoeren en vervolgens de belofte instelt om te vervullen met het resultaat van de handler of deze instelt als de handler een uitzondering geeft. Hetzelfde gebeurt als u de .catch()
-methode aanroept op een afgewezen belofte. Integendeel, roepen .then()
op een afgewezen belofte of .catch()
op een vervulde belofte zal een belofte in dezelfde staat terugkeren en de handler niet uitvoeren.
Een hangende belofte bevat een reactielijst waarbij elk reactieobject een handler of afwijzingshandler (of beide) en een reactiebelofte bevat. Dus roepen .then()
op een lopende belofte zal een reactie toevoegen met een vervulde handler en een nieuwe hangende belofte voor de reactiebelofte, die .then()
zal terugkeren. Bellen .catch()
voegt een vergelijkbare reactie toe, maar met een afwijzingshandler. Het aanroepen .then()
met twee argumenten creëert een reactie met beide handlers, en het bellen .finally()
of in afwachting van de belofte zal een reactie toevoegen met twee handlers die ingebouwde functies zijn die specifiek zijn voor het implementeren van deze functies.
Wanneer de lopende belofte uiteindelijk wordt vervuld of afgewezen, worden reactietaken gepland voor al zijn vervulde handlers of al zijn afgewezen handlers. De bijbehorende reactiebeloften zullen vervolgens worden bijgewerkt, waardoor mogelijk hun eigen reactietaken worden geactiveerd.
Voorbeelden
Overweeg de volgende code:
return new Promise(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Het is misschien niet duidelijk dat deze code drie verschillende Promise
-objecten omvat. De bovenstaande code is gelijk aan de volgende code:
const promise1 = new Promise(() => {throw new Error();});
const promise2 = promise1.then(() => console.log('Never happened'));
const promise3 = promise2.catch(() => console.log('Caught'));
return promise3;
In dit voorbeeld gebeuren de volgende stappen:
- De
Promise
wordt genoemd. - Er wordt een nieuwe
Promise
opgenomen. - De anonieme functie wordt uitgevoerd.
- Een uitzondering wordt gegooid. Op dit punt moet de foutopsporing beslissen of hij moet stoppen of niet.
- De belofteconstructor vangt deze uitzondering en verandert vervolgens de staat van zijn belofte om
rejected
met zijn waarde ingesteld op de fout die werd gegooid. Het geeft deze belofte terug, die is opgeslagen inpromise1
. -
.then()
plant geen reactietaak omdatpromise1
in derejected
status is. In plaats daarvan wordt een nieuwe belofte (promise2
) geretourneerd, die ook in de afgewezen status met dezelfde fout staat. -
.catch()
plant een reactietaak met de verstrekte handler en een nieuwe belofte in afwachting van de reacties, die wordt teruggegeven alspromise3
. Op dit punt weet de foutopsporing dat de fout zal worden afgehandeld. - Wanneer de reactietaak loopt, keert de handler normaal terug en wordt de staat van
promise3
gewijzigd om tefulfilled
.
Het volgende voorbeeld heeft een vergelijkbare structuur, maar de uitvoering is heel anders:
return Promise.resolve()
.then(() => {throw new Error();})
.then(() => console.log('Never happened'))
.catch(() => console.log('Caught'));
Dit is gelijk aan:
const promise1 = Promise.resolve();
const promise2 = promise1.then(() => {throw new Error();});
const promise3 = promise2.then(() => console.log('Never happened'));
const promise4 = promise3.catch(() => console.log('Caught'));
return promise4;
In dit voorbeeld gebeuren de volgende stappen:
- Een
Promise
wordt gecreëerd in defulfilled
staat en opgeslagen inpromise1
. - Een belofte -reactietaak is gepland met de eerste anonieme functie en de
(pending)
reactiebelofte wordt geretourneerd alspromise2
. - Een reactie wordt toegevoegd aan
promise2
met een vervulde handler en zijn reactiebelofte, die wordt teruggegeven alspromise3
. - Een reactie wordt toegevoegd aan
promise3
met een afgewezen handler en een andere reactiebelofte, die wordt geretourneerd alspromise4
. - De reactietaak gepland in stap 2 wordt uitgevoerd.
- De handler gooit een uitzondering. Op dit punt moet de debugger beslissen of ze moeten stoppen of niet. Momenteel is de handler uw enige lopende JavaScript -code.
- Omdat de taak eindigt met een uitzondering, is de bijbehorende reactiebelofte (
promise2
) ingesteld op de afgewezen status met zijn waarde ingesteld op de fout die is gegooid. - Omdat
promise2
één reactie had en die reactie geen afgewezen handler had, is de reactiebelofte (promise3
) ook ingesteld om met dezelfde foutrejected
. - Omdat
promise3
één reactie had en die reactie een afgewezen handler had, is een belofte -reactietaak gepland met die handler en zijn reactiebelofte (promise4
). - Wanneer die reactietaak loopt, keert de handler normaal terug en wordt de staat van
promise4
gewijzigd om te vervuld.
Methoden voor het vangen
Er zijn twee potentiële informatiebronnen voor vangstvoorspelling. Een daarvan is de oproepstapel. Dit is gezond voor synchrone uitzonderingen: de debugger kan de call -stack op dezelfde manier lopen als de uitzondering die de afgewikkelingscode zal ontspannen en stopt als het een frame vindt waar het in een try...catch
vangstblok. Voor afgewezen beloften of uitzonderingen in belofteconstructors of in asynchrone functies die nog nooit zijn opgeschort, vertrouwt de debugger ook op de call -stack, maar in dit geval kan de voorspelling in dit geval in alle gevallen niet betrouwbaar zijn. Dit komt omdat in plaats van een uitzondering op de dichtstbijzijnde handler te gooien, asynchrone code een afgewezen uitzondering zal retourneren, en de debugger een paar veronderstellingen moet doen over wat de beller ermee zal doen.
Ten eerste veronderstelt de debugger dat een functie die een geretourneerde belofte ontvangt waarschijnlijk die belofte of een afgeleide belofte zal retourneren, zodat asynchrone functies verder de stapel verderop zullen wachten. Ten tweede gaat de debugger ervan uit dat als een belofte wordt teruggestuurd naar een asynchrone functie, deze deze binnenkort zal wachten zonder eerst binnen te komen of te try...catch
blok. Geen van deze veronderstellingen is gegarandeerd correct, maar ze zijn voldoende om de juiste voorspellingen te doen voor de meest voorkomende coderingspatronen met asynchrone functies. In Chrome-versie 125 hebben we een andere heuristiek toegevoegd: de debugger controleert of een Callee op het punt staat .catch()
te bellen over de waarde die wordt geretourneerd (of .then()
met twee argumenten, of een keten van oproepen aan .then()
of .finally()
gevolgd door een .catch()
of een twee-argument .then()
). In dit geval gaat de debugger ervan uit dat dit de methoden zijn over de belofte die we traceren of een die hiermee gerelateerd zijn, dus de afwijzing zal worden gevangen.
De tweede informatiebron is de boom van beloftereacties. De debugger begint met een wortelbelofte. Soms is dit een belofte waarvoor de methode zijn reject()
zojuist is opgeroepen. Vaker, wanneer een uitzondering of afwijzing plaatsvindt tijdens een belofte -reactietaak en niets op de call -stapel het lijkt te vangen, volgt de debugger van de belofte die verband houdt met de reactie. De debugger kijkt naar alle reacties op de hangende belofte en ziet of ze afwijzers hebben. Als er geen reacties zijn, kijkt het naar de reactiebelofte en spreekt er recursief van. Als alle reacties uiteindelijk leiden tot een afwijzingshandler, beschouwt de debugger de belofte -afwijzing als gevangen. Er zijn enkele speciale gevallen om bijvoorbeeld de ingebouwde afwijzingshandler te dekken voor een .finally()
Call.
De belofte -reactieboom biedt een meestal betrouwbare informatiebron als de informatie er is. In sommige gevallen, zoals een oproep om Promise
Promise.reject()
In andere gevallen bevat de belofte -reactieboom meestal de handlers die nodig zijn om vangst voorspelling af te leiden, maar het is altijd mogelijk dat later meer handlers zullen worden toegevoegd die de uitzondering zullen veranderen van gevangen tot niet -ongunst of vice versa. Er zijn ook beloften zoals die gecreëerd door Promise.all/any/race
, waar andere beloften in de groep kunnen beïnvloeden hoe een afwijzing wordt behandeld. Voor deze methoden veronderstelt de debugger dat een belofteafwijzing zal worden doorgestuurd als de belofte nog in behandeling is.
Bekijk de volgende twee voorbeelden:
Hoewel deze twee voorbeelden van gevangen uitzonderingen er vergelijkbaar uitzien, vereisen ze een heel andere heuristieken van de vangstvoorspelling. In het eerste voorbeeld wordt een opgeloste belofte gemaakt, vervolgens is er een reactietaak voor .then()
gepland die een uitzondering zal geven, dan wordt .catch()
aangeroepen om een afwijzingshandler aan de reactiebelofte te bevestigen. Wanneer de reactietaak wordt uitgevoerd, wordt de uitzondering weggegooid en zal de belofte -reactieboom de vangsthandler bevatten, zodat deze wordt gedetecteerd als gevangen. In het tweede voorbeeld wordt de belofte onmiddellijk afgewezen voordat de code om een vangsthandler toe te voegen wordt uitgevoerd, dus er zijn geen afwijzingshandlers in de reactieboom van de belofte. De debugger moet naar de call -stack kijken, maar er zijn ook geen try...catch
blokken. Om dit correct te voorspellen, scant de debugger voor de huidige locatie in de code om de oproep naar .catch()
te vinden en neemt op basis van die basis aan dat de afwijzing uiteindelijk zal worden afgehandeld.
Samenvatting
Hopelijk heeft deze verklaring licht geworpen op hoe vangstvoorspelling werkt in Chrome Devtools, zijn sterke punten en zijn beperkingen. Als u foutopsporingskwesties tegenkomt vanwege onjuiste voorspellingen, overweeg dan deze opties:
- Verander het coderingspatroon in iets eenvoudiger om te voorspellen, zoals het gebruik van async -functies.
- Selecteer om op alle uitzonderingen te breken als Devtools niet stopt wanneer het zou moeten.
- Gebruik een "nooit pauze hier" breekpunt of voorwaardelijk breekpunt als de debugger stopt ergens waar je niet wilt.
Dankbetuigingen
Onze diepste dankbaarheid gaat uit naar Sofia Emelianova en Jecelyn Yeen voor hun onschatbare hulp bij het bewerken van dit bericht!