Temná strana slibů

Od vydání es6 si do NodeJS našlo cestu mnoho nových funkcí, ale žádné neměly úplně stejný dopad jako sliby. Sliby byly vyvinuty pro prohlížeč ještě předtím, než es6 vůbec existoval. Existovalo několik implementací, které byly použity jako odložený objekt jQuery, než je standard učinil zastaralými. Sliby byly na klientovi docela užitečné, zvláště pokud jste museli provádět mnoho asynchronních volání nebo pokud bylo vaše API úplný nepořádek a museli jste shromažďovat asynchronní volání z celého místa. Pro mě obvykle platilo to pozdější nebo alespoň to bylo, když jsem shledal sliby nejužitečnějšími. Schopnost předat jakýkoli slib a připojit k němu tolik zpětných volání a také je zřetězit tolikrát, kolikrát jste chtěli, poskytovala sliby vysoce univerzální, ale to bylo pro klienta. Server je jiný. Na serveru musíte provést šílené množství asynchronních volání ve srovnání s klientem. Klient normálně potřeboval pouze asynchronně volat váš server API, ale server potřebuje hovořit s databází, systémem souborů, externími rozhraními API, jako je platba a komunikace, a jakoukoli základní službou, kterou možná budete muset použít. V podstatě:hodně věcí. Jakékoli problémy, které bychom mohli mít na klientovi kvůli slibům, budou na serveru zesíleny kvůli vyšší míře využití a zvýšené pravděpodobnosti chyb.

Pokud se nejprve podíváme na kód, který používáme ke slibování, zdá se, že se příliš neliší od běžných funkcí, ale je tu jedna klíčová vlastnost, díky které jsou jedinečné. Promises zachytí všechny výjimky, které jsou v nich vyvolány synchronně. To, i když je ve většině případů velmi užitečné, může způsobit určité problémy, pokud nejste připraveni je zvládnout. Když je vyvolána výjimka, slib je odmítnut a zavolá zpětné odmítnutí, pokud nějaké existuje. Co se ale stane, když odmítnutý stav slibu nezvládneme? Záleží na verzi NodeJS, ale obecně se vytiskne varování a funkce, která vyvolala výjimku, se ukončí. Odmítání slibů prostřednictvím vyvolání výjimek je něco, co se často používalo ve starých prohlížečích knihoven slibů a je považováno za normální, ale je to vlastně dobrá věc. Je dobré nebo alespoň v pořádku, pokud skutečně chcete odmítnout slib, ale co když vyhodíte chybu ne proto, že jste chtěli, ale protože jste udělali chybu? V takovém případě musíte najít chybu a opravit ji a právě v tomto konkrétním případě by bylo opravdu užitečné nechat výjimku zřítit váš server a vytisknout trasování zásobníku. Co tedy místo toho dostaneme? V NodeJS 6 a 7 dostaneme upozornění UnhandledPromiseRejectionWarning, které vám ve většině případů řekne, co chybu způsobilo, ale ne kde. V uzlu 8 také získáme trasování krátkého zásobníku. Upgrade na uzel 8 by tedy mohl potenciálně vyřešit naše problémy, takže pokud můžete, můžete si myslet, že to je vše, co musíme udělat, abychom tento problém vyřešili. Bohužel uzel 8 zatím většina společností nepoužívá a tvoří méně než 10 % trhu.

Protože uzel 7 vám varování o odmítnutí slibu poskytne také další varování:

"DeprecationWarning:Neošetřená odmítnutí příslibů jsou zastaralá. V budoucnu odmítnutí příslibů, která nebudou zpracována, ukončí proces Node.js s nenulovým výstupním kódem."

Všimněte si, že toto varování neříká, že vyvolá výjimku, ale že bez ohledu na to zhroutí váš server. To je docela drsné, nemyslíte? Tato změna by určitě narušila nějaký kód, pokud by byla implementována dnes. Zájem o UnhandledPromiseRejectionWarning vzrostl ve spojení s popularitou a používáním slibů. Můžeme dokonce měřit, jak moc pomocí google trends.

Počet lidí, kteří hledali toto konkrétní varování, se od zavedení nativních slibů a tohoto varování do uzlu výrazně zvýšil. Během roku 2017 se počet vyhledávání zdvojnásobil, což také pravděpodobně znamená, že se také zdvojnásobil počet lidí, kteří používají sliby v NodeJS. Možná to je důvod, proč tým uzlů chce úplně vymazat varování ze svého zásobníku.

Je pochopitelné, že v případě, že se odmítnutí příslibu nevyřídí, je lepší server zřítit, než jen vydat varování. Představte si, co by se stalo s trasou API, kdyby nebylo zpracováno odmítnutí. V takových případech by odpověď nebyla odeslána klientovi, protože funkce by skončila dříve, než by dosáhla tohoto bodu, ale také by neuzavřela soket, protože server by se nezhroutil a jen by tam čekal, dokud nedojde k vypršení časového limitu. dvě minuty. Pokud by bylo na server odesláno několik takových požadavků v rozmezí dvou minut, mohli bychom velmi rychle vyčerpat zásuvky, což by naši službu navždy zablokovalo. Pokud na druhou stranu havarujeme a restartujeme, měli bychom být schopni obsluhovat některé požadavky alespoň na chvíli. Je zřejmé, že ani jeden případ není žádoucí, takže bychom měli zadat catch zpracování odmítnutí na konec každého řetězce slibů, který vytvoříme. To by zabránilo zhroucení serveru nebo vyvolání varování, což by nám také umožnilo nějakým způsobem odpovídat na požadavky API. Problém s catch metoda spočívá v tom, že jde pouze o oslavené zpětné volání odmítnutí, které se neliší od těch, které jsou poskytovány prostřednictvím druhého parametru then metoda slibu.

Největší problém, který mám se sliby, je, že všechny výjimky jsou zachyceny obsluhou odmítnutí bez ohledu na důvod, proč byly vzneseny. Je normální, že asynchronní volání mohou selhat a je normální tuto možnost zvládnout, ale zachycení všech výjimek také zachytí chyby ve vašem kódu. Když by se normálně systém zhroutil a poskytl vám trasování zásobníku se sliby, kód se pokusí zpracovat výjimku a možná selže toto asynchronní volání tiše, takže zbytek kódu bude běžet bez přerušení. Je velmi obtížné rozlišit odmítnutí slibu, které bylo vyvoláno systémem, a výjimku vyvolanou kódem, a i když byste mohli, bylo by to jen přes inženýrství. Jediný způsob, jak správně naložit se sliby, je napsat obrovské množství testů, ale skutečnost, že to prostě musíte udělat, není sama o sobě pozitivní vlastností. Ne každý to dělá a ne každému je to dovoleno a neexistuje žádný dobrý důvod, proč jim to ztěžovat.

Výjimky vyvolané v žádném asynchronním volání nemohou být zachyceny blokem try catch, takže má smysl je v případě potřeby zachytit. Klíčové slovo je zde „nezbytné“. Není nutné je zachytit během vývoje, stejně jako je expressJS nezachytí kromě výroby, ale i když je zachytí později, přinejmenším zastaví provádění kódu pro toto konkrétní volání, což nemůžete udělat pro sliby. Správný způsob, jak zpracovat výjimky ve slibech nebo v jakýchkoli jiných asynchronních voláních, je (a) poskytnout jim obsluhu výjimek, která, pokud je poskytnuta, bude provedena, pokud je vyvolána výjimka, a (b) zastavit řetězec slibů nebo zbytek kód od spuštění. Tento obslužný program lze šířit v řetězci příslibů a pokud není nastaven, umožní výjimce probublávat a zřítit server.

Někteří lidé si myslí, že k vyvolání zpětného volání o odmítnutí je nutné vkládat sliby, ale to nikdy nebyla pravda. I dnes stačí vrátit Promise.reject(someError) abyste nesplnili jakýkoli slib tam, kde byste normálně udělali throw . Pokud jste se zeptali, proč se chyby házení používají k odmítnutí slibů, mnoho lidí by nedokázalo odpovědět. Nejsem si jistý, jestli na začátek existuje jiná odpověď, než že to byl způsob, jakým byly před mnoha lety implementovány sliby pro prohlížeč, a ECMA právě reimplementovala tento poněkud narušený standard do ES6 a Node to odtud převzal. Byl dobrý nápad zavést tuto verzi slibů do standardu a migrovat ji na stranu serveru? Skutečnost, že se Node vzdaluje od standardu, by v nás měla vyvolávat určité pochybnosti. Není ani pravda, že sliby jsou jediným způsobem, jak zvládnout to obávané peklo zpětného volání. Existují i ​​jiná řešení, jako je async a RQ například knihovny, které zahrnují metody jako parallel a waterfall které umožňují kodérům provádět asynchronní volání organizovanějším způsobem. Přinejmenším na straně serveru je poměrně vzácné potřebovat více než nějakou kombinaci metod, které tyto knihovny poskytují. Důvod, proč byly sliby zavedeny do standardu, mohl být jednoduše proto, že byly populární díky jQuery. Implementace zpracování výjimek by byla snazší s tradiční asynchronní knihovnou, ale to neznamená, že to nelze provést se sliby. I dnes můžete přepsat then metoda na prototypu Promise a konstruktoru Promise, který to udělá.

Promise.prototype.then = (function () {
  const then = Promise.prototype.then;
  const fixCall = function(promise, next){
    if (!next) {
      return null;
    }
    return function (val) {
      try {
        let newPromise = next.call(promise, val);
        if(newPromise){
          newPromise.error = promise.error;
        }
        return newPromise;
      } catch (exception) {
        setTimeout(function () {
          if (promise.error) {
            promise.error(exception);
          } else {
            throw(exception);
          }
        }, 0);
        return new Promise(()=>{});
      }
    }
  };
  return function (success, fail, error) {
    this.error = this.error || error;
    let promise = then.call(this, fixCall(this, success), fixCall(this, fail));
    promise.error = this.error;
    return promise;
  }
}());
function createPromise(init, error){
  let promise = new Promise(init);
  promise.error = error;
  return promise;
}  

Již jsem zmínil, že asynchronní volání nelze zachytit blokem try catch a to platí i uvnitř příslibu, takže je možné se od příslibu vymanit pomocí setTimeout nebo setImmediate volání. Pokud tedy zachytíme výjimku, uděláme to, pokud nebyl poskytnut obslužný program výjimky, v takovém případě to místo toho nazýváme. V obou případech chceme zastavit provádění zbytku řetězce slibů a můžeme to udělat jednoduše vrácením prázdného slibu, který se nikdy nevyřeší. Je zřejmé, že tento kód je zde pouze proto, aby demonstroval, že to lze provést, a přestože nyní můžete správně zacházet s výjimkami, neztratili jste žádnou z původních funkcí.

Jedním z hlavních problémů slibů je, že je možná používáte, aniž byste si to uvědomovali. Existuje několik populárních knihoven, které používají přísliby v zákulisí a zároveň vám umožňují specifikovat tradiční zpětná volání, ale provedou je uvnitř příslibů, které používají. To znamená, že jakákoli výjimka bude zachycena bez vašeho vědomí nebo schopnosti přidat reject handler pro ně, takže zatím zvýší UnhandledPromiseRejectionWarning. Určitě se poškrábete na hlavě, pokud uvidíte toto varování, aniž byste měli ve svém kódu jediný slib, stejně jako jsem to udělal před časem. Normálně byste ve varování dostali relativně užitečnou chybovou zprávu, ale pokud spouštíte špatný kód v metodě asynchronní knihovny, pravděpodobně selže způsobem, který většina z nás nedokáže pochopit. Jakmile zadáte příslib, všechna vaše zpětná volání budou provedena v kontextu tohoto příslibu a pokud se z toho nevymaníte pomocí něčeho jako setTimeout převezme celý váš kód, aniž byste si to uvědomovali. Uvedu zde příklad, který používá starší verzi modulu Monk MongoDB. Tato chyba byla opravena, ale nikdy nemůžete vědět, zda jiná knihovna udělá něco podobného. Takže když víte, že mnich používá sliby, co si myslíte, že se stane, když spustím tento kód na prázdné databázi?

async.parallel({
  value: cb => collection.find({}, cb)
}, function (err, result) {
  console.log(result.test.test); //this line throws an exception because result is an empty object
});

Odpověď zní:

(node:29332) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): Error: Callback was already called.

Pokud nepoužíváte Node 8, v takovém případě získáte:

(node:46955) 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:46955) UnhandledPromiseRejectionWarning: Error: Callback was already called.
    at /node_modules/async/dist/async.js:955:32
    at /node_modules/async/dist/async.js:3871:13
    at /node_modules/monk-middleware-handle-callback/index.js:13:7
    at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)

Hodně štěstí při hledání příčiny 😊.

Zdroje:

  1. https://semaphoreci.com/blog/2017/11/22/nodejs-versions-used-in-commerce-projects-in-2017.html
  2. https://trends.google.com/trends/explore?date=2016-03-30%202018-03-30&q=UnhandledPromiseRejectionWarning
  3. https://github.com/nekdolan/promise-tests