Tutoriál pro zpracování asynchronních chyb pomocí try/catch promise v

  • Pochopení JavaScriptového enginu, zásobníku volání a front úloh je klíčem k předpovídání, jak a kdy se obslužné rutiny chyb spustí v asynchronním kódu.
  • Promisy zavádějí „neviditelný try/catch“, který zachycuje výjimky a umožňuje řetězení. then y catch spravovat chyby centralizovaným a čitelným způsobem.
  • Syntaxe async/await zjednodušuje ošetření chyb pomocí bloků try/catch klasiku a upřednostňuje sekvenčnější psaní asynchronního kódu.
  • Globální události, jako např. unhandledrejection a správné vzorce složení pomáhají předcházet neošetřeným odmítnutím a navrhnout robustní strategii protokolování a oznamování chyb.

ošetření asynchronních chyb pomocí try/catch

Správné ošetření asynchronních chyb v JavaScriptu tvoří zásadní rozdíl mezi profesionální aplikací a aplikací, která selže při sebemenší provokaci. Když začnete používat Promises a async/await, běžným přístupem je použití bloků try/catch, „protože je to nutné“, ale bez jasné strategie je snadné skončit s tichými chybami, neužitečnými zprávami pro uživatele a varováními „Neošetřené odmítnutí promise“ v celé konzoli.

V tomto článku se klidně a s praktickými příklady podíváme na to, jak strukturovat Kompletní tutoriál pro zpracování asynchronních chyb pomocí promise try/catch vJak funguje JavaScriptový engine "pod kapotou", jakou roli hrají fronty úloh a mikroúlohy a jak je správně kombinovat .then(), .catch() y async/await aby se předešlo nevyřešení jakýchkoli chyb, a to jak v prohlížeči, tak v Node.js.

Jak JavaScript funguje interně a proč ovlivňuje ošetřování chyb?

Než se ponoříme do ošetřování chyb pomocí promisů, je užitečné si něco málo porozumět JavaScriptovému enginu, jeho zásobníku volání a běhovému prostředí . To vysvětluje, proč se určité chyby objevují „později“ nebo proč se promisy chovají jinak než klasické zpětné volání.

Jak aplikovat záplaty na kód pomocí utility patch
Související článek:
Nejlepší aplikace, které se naučíte programovat z mobilu

Prováděcí prostředí, kontext a zásobník volání

JavaScript běží v jednom vlákně (alespoň koncepčně), což znamená, že engine může dělat vždy pouze jednu věc. jednu věc najednou na zásobníku voláníAby se zabránilo blokování celého webu během čekání na síť, disk nebo DOM, prohlížeče zpřístupňují Web APIs které umožňují asynchronní operace a které později vracejí výsledek prostřednictvím zpětných volání, promise nebo async/await.

Veškerý kód JavaScriptu někde žije kontext provedeníMáme globální kontext (první, který se vytvoří při načtení skriptu nebo stránky) a pokaždé, když je funkce vyvolána, vytvoří se nový kontext funkce s jejími proměnnými, jejími this a odkaz na externí kontext. Tyto kontexty jsou naskládány v zásobník volání (zásobník volání) podle zásady LIFO: poslední funkce, která vstoupí, je první, která vystoupí.

Když voláte funkci jako first()Toto je umístěno na zásobníku; pokud je uvnitř, volá second() a toto k third()Uvidíte, jak zásobník neustále přidává a odebírá kontexty a každý Zpráva konzole odráží pořadí vstupu a výstupu ze zásobníku.Nástroje, jako je ladicí program prohlížeče, zarážky nebo console.trace() Umožňují vám prohlédnout si daný zásobník v reálném čase.

Fronty úkolů a mikroúkoly: proč se sliby provádějí „později“

JavaScriptový engine také spolupracuje s několika fronty úlohNejrelevantnější jsou zde fronta makroúkolů a fronta mikroúkolů. Fronta makroúkolů zahrnuje například setTimeout, setInterval nebo události DOM. Fronta mikroúkolů zahrnuje mimo jiné zpětná volání promibů a pozorovatelé mutací DOM.

Cyklus událostí funguje takto: spustí se synchronní kód, poté se zpracuje makroúloha a nakonec fronta mikroúkolů (všechny čekající úlohy)a teprve poté začíná další makroúkol. Proto then() nebo catch() spojený s příslibem je spuštěn před setTimeout(..., 0), ačkoliv se obojí zdá být „bezprostřední“.

Tento detail je klíčový pro pochopení, kdy obslužné rutiny chyb promise A proč se chyba, která se vyskytne v rámci .then() Nezhroutí váš skript okamžitě, ale objeví se v pozdější fázi cyklu událostí.

Zpětná volání, peklo zpětných volání a skok k Promises

Historicky byly asynchronní operace v JavaScriptu vždy ošetřovány pomocí zpětných volání . Jedna funkce přijímala jako argument jinou funkci a provedla ji po dokončení úlohy: požadavky na server, časovače, čtení souborů pomocí File API, operace s IndexedDB atd.

Představte si funkci fetchData(callback) který interně volá setTimeout simulovat síťový požadavek a po několika sekundách vyvolat zpětné volání s datyMezitím program pokračuje v provádění a zobrazuje zprávy jako „Data se načítají…“. To ilustruje, že zpětné volání se provede později, když událost dorazí do fronty a engine ji zpracuje.

Peklo zpětných volání: když se zpětná volání stanou nezvládnutelnými

Když začnete řetězit operace, které na sobě závisí, věci se zkomplikují. Například zpracováváte kliknutí na tlačítko, zadáte požadavek pomocí XMLHttpRequest (bez slibů), používáte setTimeout Chcete-li odložit zpracování, načtete soubor z <input type="file">a pak výsledek uložíte do indexedDBKaždé z těchto API očekává jiné zpětné volání.

Pokud se pokusíte vše koordinovat pomocí vnořených zpětných volání, skončíte s nechvalně známou „pyramidou zkázy“ nebo peklem zpětných volání : spousta odsazení, obtížné čtení, téměř nemožná údržba a noční můra při ošetřování chyb, protože musíte opakovat logiku chyb na každé úrovni.

Tento scénář byl jedním z hlavních důvodů pro zavedení promisů v JavaScriptu: deklarativnější a lépe spravovatelný způsob reprezentace asynchronních operací a jejich potenciálních selhání.

Promisy: stavy, řetězení a ošetření chyb

ošetření asynchronních chyb pomocí try/catch

A Promise Je to objekt, který reprezentuje budoucí výsledek asynchronní operaceMůže se úspěšně dokončit nebo selhat. Interně má tři stavy: čekající, dokončeno (vyřešeno) nebo odmítnuto. Konstruktor Promise Dostává funkci „exekutora“, která následně dostává dvě funkce: resolve y reject.

Například můžete vytvořit slib, který po náhodném čase zavolá resolve() o reject() podle určité podmínkyPak pomocí then()Definujete, co dělat, když je slib splněn, a s catch() Co dělat, když je odmítnuto. Tento vzor umožňuje okamžitě vrátit objekt (promise), který bude fungovat jako „kontejner“ pro daný budoucí výsledek.

Konzumace slibů: then, catch a řetězení

Moderní funkce obvykle vracejí promise přímo. Místo samostatného předávání zpětných volání pro úspěch a neúspěch obdržíte slib, ke kterému "připojíte" zpětná volání s then() y catch()Například funkce crearArchivoAudioAsync() Mohlo by to vrátit promise místo čekání na staromódní zpětná volání.

Pravá síla přichází s řetězec slibů. Metoda then() Vždy vrací nový promise. To znamená, že můžete napsat věci jako:

hazAlgo() následuje .then(hazAquello)což zase vrací další slib, ke kterému ho připojíte .then(hazOtraCosa)a tak dále. Každý krok představuje dokončení předchozího a pokud některý z nich zpětná volání vrací další slibŘetězec se automaticky upraví a čeká na vyřešení nového slibu.

S tímto vzorem se to, co dříve bylo pyramidou vnořených zpětných volání, stává plochý řetězec asynchronních operacímnohem čitelnější. Chyby lze navíc centrálně řešit pomocí jednoho .catch() na konci řetězce.

„Neviditelný pokus/úlovek“ v promisech

Jedním z klíčových bodů promise je, že interní vykonavatel a obslužné rutiny (then, catch) obsahovat „Zkus/chyti neviditelného“. Například při použití šipka a tato funkce v JavaScriptuJakákoli výjimka vyvolaná v rámci promisu nebo v kódu obslužné rutiny automaticky vede k odmítnutí promisu.

Následující kód tedy:

new Promise((resolve, reject) => { throw new Error("oops") })

je funkčně ekvivalentní explicitnímu volání reject(new Error("oops"))Totéž platí pro zpětná volání .then()Pokud ano throw Uvnitř se nespadne celý skript; výsledný promise se místo toho přepne do stavu zamítnutí a řízení se přesune na další obslužnou rutinu chyb v řetězci.

Ošetření chyb: zachycení, šíření a opětovné vyvolání

Metoda .catch() Zachycuje veškerá odmítnutí, ke kterým dojde v předchozím promisu nebo v jakémkoli promisu nad ním v řetězci, za předpokladu, že chyba ještě nebyla ošetřena. Můžete umístit .catch() na konci dlouhého řetězce .then() centralizovat ošetření chyb, stejně jako byste měli jeden try..catch zabalení několika bloků kódu.

V .catch() Můžete analyzovat typ chyby (například rozlišit mezi URIError nebo jiné programátorské chyby), řešte to, pokud víte, jak na to, a pokud to nezvládnete, opětovné uvolnění chyby pomocí throwTímto způsobem se řízení přesune na další obslužnou rutinu. .catch() řetězce, což umožňuje návrh vrstevnatých strategií obnovy.

Můžete si také vytvořit .catch() Úspěšně „uzavře“ chybu (bez jejího opětovného vyvolání) a vrátí hodnotu pro zotavení. V takovém případě řetězec pokračuje v normálním průběhu a následuje další then Běží, jako by všechno proběhlo dobře, a dostává onu „alternativní“ hodnotu.

Mějte na paměti, že .then() Může přijímat dva argumenty: první pro úspěch a druhý pro neúspěch. Tento druhý parametr funguje jako lokální obslužná rutina chyb pro danou fáziV praxi se však častěji používá .catch() oddělené, protože to usnadňuje přehlednější tok chyb.

Co se stane s neošetřenými chybami?

Pokud je slib odmítnut a neexistuje žádný obsluha chyb (.catch() nebo druhý argument .then()) V řetězci se chyba „zasekne“ a engine ji považuje za neošetřené odmítnutí. To je docela podobné tomu, co se stane se synchronní výjimkou, která se nenachází v bloku. try..catchSkript se "spustí" a v konzoli se zobrazí zpráva.

V moderních prohlížečích můžete tyto chyby zachytit globálně pomocí události unhandledrejectionTato událost získává PromiseRejectionEvent s nemovitostí promise (slib, který selhal) a reason (příčina odmítnutí). S tímto můžete Zaznamenat chybu na server a zobrazit uživatelsky přívětivou zprávu nebo alespoň zabránit tomu, aby selhání zůstalo bez povšimnutí.

Také se koná událost rejectionhandledToto se spustí, když odmítnutý slib později obdrží obslužnou rutinu chyby. Obě události umožňují vývoj komplexní strategie pro sledování a zpracování chyb slibů.

V Node.js prostředí generuje podobné události (unhandledRejection), které se ve výchozím nastavení zaznamenávají do konzole. K těmto událostem se můžete připojit, abyste centralizovat protokolování chyb nebo zabránit tomu, aby byl standardní výstup zaplněn nekontrolovanými stopami.

Praktické vzory pro ošetření chyb pomocí Promises

Pro zajištění robustního zpracování chyb u promisů je důležité dodržovat určité vzorce kompozice a vyhýbat se typickým chybám, které narušují řetězec kontroly.

Vždy se sváži a vrať sliby

Velmi častou chybou je vytváření slibů v rámci .then() ale nevrátí ho. Pokud to uděláte, řetězec se „rozdělí“ na dvě části: na jedné straně pokračuje původní slib a na druhé straně nový interní slib, aniž by to ovlivnilo hlavní sekvenci.

Jotta-cli v Linuxu
Související článek:
Jak naplánovat automatické úlohy pomocí Crontabu v Linuxu

Zlaté pravidlo zní „Pokud v rámci obslužné rutiny vytvoříte promise, vraťte ho.“. Tedy, then Vyšší autorita bude čekat na ukončení (vyřešení nebo zamítnutí) tohoto interního slibu a chyby budou sledovat jedinou cestu směrem k .catch() společné, místo aby byly rozptýleny mezi samostatnými obslužnými rutinami.

Je také dobrý nápad ukončit řetězy pomocí .catch() Přečtěte si prosím pečlivě tento dokument, abyste pochopili naše zásady a postupy týkající se vašich údajů a toho, jak s nimi budeme nakládat. Pokud s našimi zásadami a postupy nesouhlasíte, máte možnost nepoužívat naše stránky. Přístupem na stránky nebo jejich používáním souhlasíte s těmito Zásadami ochrany osobních údajů.Tímto způsobem zajistíte, že všechny chyby, které nebyly dříve ošetřeny, budou v určitém okamžiku zaznamenány, a zabráníte tak tomu, aby se z nich globálně staly nespravované odmítnutí.

Vyhněte se zbytečnému vnořování then a catch

Dalším klasickým anti-vzorem je vnořování .then() jeden uvnitř druhého místo jejich propojení dohromady. Nadměrné vnořování vede k rozsah chyb matoucí a obvykle to jde ruku v ruce s předchozí chybou (nevracení interních promisů).

Vnořování má smysl pouze tehdy, když chcete psovod catch interní pokrývá pouze určitou část řetězceNapříklad můžete vnořit blok, který zahrnuje řadu „volitelných“ kroků a mít catch věnované zachycení pouze chyb z dané sekce, zatímco a catch Vnější část je vyhrazena pro kritické chyby ve zbytku toku.

V tomto vzoru jsou chyby v rámci volitelného bloku ošetřeny v catch interní a po vyřešení může provádění pokračovat s následnými kritickými úlohami. Pokud dojde k chybě v kritické části před volitelným blokem, je tato chyba absorbována výhradně catch konec řetězu.

Zabalení starších callback API do promisů

Mnoho starších API, jako například setTimeout() Některé starší knihovny stále očekávají „staromódní“ zpětná volání a nevrací promise. Pokud tyto funkce voláte přímo v kódu založeném na promisech, ztratíte schopnost rovnoměrně zachytit chyby.

Řešením je zabalit tato API do konstruktoru typu PromiseNapříklad si můžete vytvořit delay(ms) který po určité době interně vrací splněný slib setTimeoutOd té doby vždy používáte zabalená funkce, která vrací promiseNikdy nepoužívejte staré API, aby veškerý váš asynchronní kód byl „promise-friendly“.

Kromě new Promise()Máte zkratky jako např. Promise.resolve() y Promise.reject() pro rychlé vytváření promibů, které již byly vyřešeny nebo odmítnuty, což je užitečné pro skládání nebo testování.

Pište sliby paralelně nebo postupně

Chcete-li spustit více asynchronních operací paralelně a čekat na jejich dokončení, musíte Promise.all()Tato metoda přijímá pole promise a vrací slib, který je vyřešen, když všichni uspějí, nebo je odmítnut, jakmile jeden z nich selže.

Pokud máte pole a chcete pro každý prvek vydat promise, můžete použít .map() transformovat ho do pole promise a předat ho Promise.all()Tento vzorec je ideální, když vám nezáleží na pořadí, v jakém je řešíte, hlavně když jsou všechny dokončeny.

Pro sekvenční skládání s poli můžete použít reduce() postavit řetězec slibů ekvivalentní Promise.resolve().then(func1).then(func2)...Je dokonce možné zapouzdřit tento vzor do kompoziční funkce (composeAsync), která přijímá několik funkcí (synchronních nebo asynchronních) a vrací novou funkci, která je všechny zřetězí dohromady pomocí počáteční hodnoty.

V moderním ECMAScriptu se mnoho z těchto kompozic stává přirozenějšími pomocí async/await, jak nyní uvidíme.

Async/Await: čistší syntaxe se stejným modelem chyb

Klíčová slova async y await Poskytují čitelnější způsob práce s promisy. funkce označená jako async Vždy vrací promise, i když se interně zdá, že vracíte normální hodnotu. A await Umožňuje vám „pozastavit“ provádění dané funkce, dokud není slib vyřešen nebo odmítnut.

Například funkce async function getUser() můžeš zavolat await fetch(...) a pak na await response.json()a nakonec vrátit výsledný objekt. Venku kdokoli vyvolá getUser() obdrží slib, který je s těmito daty vyřešen.

Ošetření chyb s try/catch v asynchronních funkcích

Velkou výhodou async/await Můžete použít bloques try/catch "celoživotní" pro zpracování asynchronních chyb. Pokud je očekávaný promise s await Je odmítnuto, ve funkci je vyvolána výjimka. async, s nimiž se dá chytit catch.

Díky tomu je zpracování chyb velmi podobné synchronnímu kódu: seskupujete await kritici uvnitř try, analyzujete výjimka v catchZaznamenáváte technické detaily pro ladění, zobrazujete uživateli srozumitelnou zprávu a v případě potřeby znovu vyvoláte chybu, aby se vyšší vrstvy mohly rozhodnout, co dělat.

Můžete mít také blok finally který běží vždy, ať už došlo k chybě, nebo ne, ideální pro uvolnění zdrojů, resetování stavů načítání nebo uzavření dočasných připojení.

Sekvenční provádění a závislosti mezi voláními

Když jedna operace závisí na výsledcích jiné, await Výrazně to zjednodušuje pracovní postup. Můžete provést první volání pro shromažďování informací o uživateli, extrahovat odtud URL jejich repozitářů a poté spustit druhý požadavek pomocí await, zajištění toho, aby se druhý spustil až po úspěšném dokončení prvního.

Tento styl zabraňuje šíření .then() vnořený a kód působí téměř synchronně, ačkoliv pod povrchem stále používá promisy a stejný model provádění založený na frontách mikroúkolů.

Čekání na nejvyšší úrovni v modulech

Moderní ES moduly umožňují použití await přímo na vyšší úroveňbez nutnosti zabalit kód do funkce asyncToto je určeno pro případy, jako je načtení počáteční konfigurace, získání dat potřebných pro vykreslení nebo provedení požadavku. fetch jehož výsledek se okamžitě použije k naplnění globálních struktur modulu.

S tímto přístupem můžete napsat něco jako const colors = await fetch(...) na začátku souboru a modul nebude považován za plně vyhodnocený, dokud nebude tento slib vyřešen. Model chyby zůstává stejný: pokud je tento slib odmítnut a není ošetřen, bude se šířit jako chyba modulu.

Vztah mezi Promises a globálním ošetřením chyb

Promisy řeší jeden ze základních problémů modelu založeného na zpětných voláních: zachycení všech chyb, včetně programovacích výjimek , ve struktuře, která usnadňuje funkční složení asynchronních úloh.

Když je slib odmítnut a nikdo se o něj nezabývá, běhové prostředí vygeneruje příslušné globální události (unhandledrejection v prohlížeči, unhandledRejection (v Node.js). To je velmi užitečné pro vytvoření poslední vrstvy obrany: zaznamenání chyby, upozornění monitorovacího systému, zobrazení obecné zprávy uživateli nebo alespoň zabránění nekontrolovatelnému zahlcení konzole těmito chybami.

Zavolej event.preventDefault() v těchto obslužných rutinách můžete zakázat výchozí chování prostředí (například výpis trasování do konzole), za předpokladu, že jste se rozhodli to řešit sami jiným způsobem. V každém případě je vhodné pečlivě zkontrolovat, které promisy jsou odmítány a zda se nejedná o skutečné programátorské chyby, než je ignorujete.

Nejlepší postupy pro robustní strategii asynchronního zpracování chyb

Pro úplnost je vhodné shrnout několik pokynů, které pomáhají při robustní systém pro ošetření chyb v asynchronním kódu s promisy a async/await:

  • Místo .catch() přesně tam, kde víš, jak reagovatNení povinné zaznamenávat všechny chyby; existují neopravitelné chyby, u kterých je nejlepší nechat chybu eskalovat a monitorovat ji pouze globálně.
  • Využijte druhého argumentu .then() pouze tehdy, když potřebujete velmi lokální léčbuVe většině případů .catch() Jeden řetězec je čitelnější a méně náchylný k chybám.
  • Nezahlcujte uživatele technickými detailyV rozhraní zobrazuje jasné a uživatelsky přívětivé zprávy a technické detaily (trasování zásobníku, datové části atd.) si vyhrazuje pro interní logovací nebo monitorovací systémy.
  • Centralizovat protokolování chybAť už se jedná o globální nespravované události promise, middleware, HTTP interceptory nebo sdílené utility, snažte se mít co nejméně bodů, kde se rozhoduje o tom, co a jak se bude protokolovat.
  • Vždy dodržujte sliby, kterým věříte, a přetrhněte s nimi řetězy catchDíky tomu se drasticky snižuje počet nezachycených chyb a tok řízení je předvídatelný.
  • Herectví async, nezapomeň awaitOpomenutí může způsobit, že odmítnutý slib nebude vázán try/catchutíkat na místa, kde byste to nečekali.
ošetření asynchronních chyb pomocí try/catch
Související článek:
Nejlepší IDE pro programování ve Windows

Spojení slibů, try/catch a globální události, jako např. unhandledrejectionJe možné vytvářet aplikace, kde jsou asynchronní chyby pod kontrolou, uživatel dostává jasné zprávy a vývojový tým má potřebné informace k ladění a vylepšování kódu, aniž by aplikace „umírala“ bez vysvětlení uživateli. Sdílejte tuto programátorskou příručku a pomozte ostatním uživatelům vyřešit asynchronní chyby pomocí funkce Try/catch.


Přidat jako preferovaný zdroj v Googlu