Галоўная / Артыкулы / Пасуючыя думкі пра async/await, які спрычыняюць багі ў працоўным сераверы

Пасуючыя думкі пра async/await, які спрычыняюць багі ў працоўным сераверы

Пасвячана дзевяцьму тонкім непрацэспадным/await недазерам — ад узгоджаннях выконання да неадмініструваных адказоў — які таямна разбивают рэальныя JavaScript-застосункі.

3620 слоў

Дзеўнае часа багатыя разработчыкі прыпускаюць, што ў яных є моцнае розумэнне async/await, проста таму, што яны вжываюць яго аднойчы. Знанне таго, што функцыя async завжды вяртае прабаму, знанне таго, калі трэба ставіць await перед вызовам, які з’яўляецца ў сети, і знанне таго, што try/catch дапамагае лепш адмаўляцца асінхронным неудачам, можа здавацца дастатнім. Параўняна з кодам, які активна вжывае калбэкі, такі стыль выглядае апранутым, і ў яго легка чытаць, гэта практычна звычайны сінхронны JavaScript: запрашаецца корыстнік, завантажуецца яго аблік, апшырана UI, і ловяцься всі проблемы, якія можу выйсці па дорозе. Сынтаксіс такі інтуўітывны, што легка не зупыняцца і не аналізаваць ментальную модель, якая стоіць за ўсім гэтым.

Асалідныя проблемы заключаюцца у тым, што такая паверхневая вольнае владанне мовай пакрывае толькі форму async/await, а не фундаментальныя правілы асінхроннага керавання. Часта ўпадчывасць — спрыяваць await так, нібы ён замарожвае весь програм, думаць, што вызов асінхронной функцыі автаматычна прыяўляе яе рэзультат да таго, хто ёю вызваў, і верыць, што код, напісаны лінейным, зверху-вниз стылем, не можа конкуруваць з самім сабою. Гэтыя упадчывасці рэдка калі спрычыняюць видныя наследкі ў маленькіх дэманстрацыях, адтуды ўсё робіцца адразу, адпаведзі прыходзяць быстра, а дадзеныя для тэставання обробляюцца у прыгадным порядку. Рэальныя продакшн-сістэмы значна менш пацерпяльныя.

Багі, які ў канцэ наступаюць, не нагадваюць стандартныя помылкі асінхронной працы. Функцыя вяртаеся раней, чым паводзіныя ей наследкі фактычна завершаюцца. Стары запит перакрывае стан, заданы новым. Блок try/catch не дазволяе перакрыць адмову. Блок пакетных заданняя запускае больш адночасных заведама, чым система можа рэальна абсорбаваць. Кожная прадзеякт, якую рассматрываюць окрема, выглядае абсалютна нормальна — пры тым як весь процес работы зовсім не падобаецца таму, што спачатку было запланавана.

Можа прыйсці даўно, каб пачуць усё настаўнік: async/await не пазбавляе JavaScript можлівасця адночаснага выканання. Ён проста дае адной асінхронной функцыі болей чытальны спосаб зупінкі та продажвання выканання. Усё інша яшчэ залежыць ад таго, якія обявы будуць вярнуты, якія будуць чаканы, якія будуць тыха празрэшчаны, якія будуць анульваны чы перапрыбуткаваны, і якія могу зменіць спакоюемы стан працы.

Я думаў, што await зупіняе больш адной функцыі

Паўзлівая першая хыбная думка ёсць простая. Калі бярза з’яўляецца await, хочацца ментальна спрыягчы гэта як зупінку всього, што адбываецца ў яго апошніне. JavaScript ніколі не блакуе вікна браузера чы процэс Node.js, але лёгка ўсё ж такі прыпускати, што адночаснае выкананне будзе чакаць своей разы.

Так гэта не працюе. await толькі призначаны для паўзы конкрэтнай асінхронной функцыі, якая знаходзится ўнутры яе. Контроль вяртаецца да среды выконання, пакуль запрашваная обяцанне ўсё яшчэ не выконана, а JavaScript продовжвае обрабоцваць все іншае, што готава да выконання. Слухачы змян можаць запрацаваць, таймеры могу спрацаваць, некалякія запросы можаць быць выкаананы, а праз гэты час можа таксама запусціцца ўзьвышэйшы вызов той самай функцыі.

async function loadProfile() {
  console.log("Loading profile");

const profile = await fetchProfile();
  console.log("Profile loaded");
  return profile;
}
console.log("Before");
loadProfile();
console.log("After");

Нічога ў зовнішнем коде не чакае толькі таму, што loadProfile мае ў сабе await. Вызов функцыі запускае яе і негайна вяртае обяцанне. Як толькі той, хто яе вызваў, не выберае запрацаваць з гэтым обяцаннем або яго вярнуць, выконанне проста продовжваецца без паўзы.

Гэта можа здавацца незначным тэхнічным моментам, але яго наследкі вялікія — ўсё не толькі паспаленыя лог-запісы консолі. Обробнік запитоў можа адправіць свой адпаведзень, калі запіс у журнале аудыту ўсё яшчэ трывае. Інструменты кліентскага рядка можу завершыцца, калі запіс у файле ўсё яшчэ чакае на адпрацоўкі. Тест можа паведаміць пра успех, перш чым будуць выкананы запэчаткуванні, якія знаходзяцца ўнутрь асінхроннага калбэка. Асінхронная функцыя працуе абочна так, як і было запланавана — справжняя проблема ў тым, што вызывач ніколі не павязаў свое завершэння з завершэнням асінхроннага вызыву.

Таму важлівыя запитанне — не тое, чы рэалізуе функцыя await у сабе, а тое, чы обявленая праміса, якая представляе цэлую операцыю, дасканальна павязана з кодам, які трэба інформавацыю пра ўсуненне операцыі.

Я вызываў асінхронныя функцыі, не вялічы, хто будзе адпаведальны за ўсуненне іх роботы

Адзін часты глытак — это запуск асінхронной функцыі без чакання ў яе рэзультат або вярнення яго. Інодзе гэта проста недбаласць. Іншыя разы задача здаецца настолькі незначной, што ёй проста дазволяюць выконвацца на фоне.

Настоямы бяглы не ў тым, што задача выконваецца незалежна — а ў тым, што ніхто насправдзе не вярнуўся да таго, хто будзе адпаведальны за яе рэзультат.

Уявіце зберэжанне замовлення, пасля чаго надаецца абавесць. Якщо гэтая абавесць прынцыпова павінна быць адправлена, каб уся операцыя зарачунвалася завершанай, тады той, хто вызваў функцыю, павінен чакаць на яе рэзультат. Якщо гэта справды неабавесны пункт, можна дазволіць яй выконвацца самае, але рэзультат ўсё раве павінен кудысь паехаць. Проста адкінуць вярнутую прамісу не ператворыць вызов у надзеяны фонавы задача — гэта проста пазбавіць таго, хто вызваў, можлівасці дазнацца, што сталося пасля таго.

Гэта становіцца ўсё болей рызыкаваным у коде сервера. Функцыя можа адправіць успешны адпаведзень нават тады, калі якісь пазнейшы асінхронны наследкі не выйшлі. Пользователь будзе думаць, што операцыя завершылася успешна, тады як у системе няма стойкага запісу пра тое, што робота ўсё яшчэ не завершылася. Якщо ў той момент процес зноў запускаецца, гэта незавершанае заведамства проста зникае.

Дапамагае спрыяць тому, каб кожную незачаканую обяцанню спрыймалі як архітектурны выбор, а не як паслядню думку. Альбо ныякая операцыя несе адпаведальнасць за рэзультат і должна на яго чакаць, альбо іншы слой ёю керуе і патрэбуея правільнага механізма для стежыць за завершэнням, перапрыбуткамі і абяканнямі. Няма нічога пасаблівага ў справжняй намеровай роботе на фоне — але яна не должна з’яўляцца проста таму, што хтось забыў напісаць await.

Прагненне, якое ніхто не стежыць, па свайму прыродзе не ўважаецца фонавой задачай. Цэў проста работа без власніка.

Адносаванне forEach да прагнень, як да чагось, што іх разуме

Аднам з першых асінхронных патэранаў, якія таямна наражаюць на пагрозу звычную ментальную модель, ёст тое, калі forEach спаўнаеся разам з асінхронным калбэкам. Сынтаксі выглядае абсолютна разумна, што спакойна выклікае нерозумэнне таго, чаму функцыя-акуратчык завершаецца раней, чым фактычна выконваецца якая-лібо робота.

async function saveUsers(users) {
  users.forEach(async (user) => {
    await saveUser(user);
  });

  console.log("All users saved");
}

Паведамленне пра завершэння можа практычна з’явіцца раней, чым будзе захаваны хоць адны запис пользователя. Прычына ў тым, што forEach вызывае калебэк, але відкидае все, што той вяртае. Асінхронны калебэк завжды вяртае праміс, таму кожны вызов таямніча стварае праміс, да якога ніхто не зберагае канектар. Калі няма чаго чакаць, зовнішня функцыя негайна завершвае свою роботу, незалежна ад таго, што яшчэ робяць ўсі елементарныя вызовы.

Гэта на самай справе не ўласна баг forEach. Ён вынікае з припуску, што передача асінхронной функцыі будь-якаму методу масі автаматычна падніжае стандарты цього методу так, юнак яны разумеюць прамісы. Аднаўлення не адбываецца. Памочны прыемнік для сінхронной ітераціі застаецца сінхронным, незалежна ад таго, які тип функцыі ў яго передаецца, хоць бы толькі гэты прыемнік не быў спецыяльна створаны для коордынавання асінхронных задач.

Тая жа проблема выступае ў функціях map, filter і reduce. Выкарыстоўванне map з асінхронным калбэкам вяртае масэвую, заповненую обявленымі прамісамі, а не масэўку фінальных значэнняў. filter ніколі не чакае на асінхронны прабет, таму ён ацэнюе самыя об’екты прамісаў — якія завжды ўважаюцца істэнчнымі — замест тых рэзультатаў, якія ў канцы канцоў выдаюць. Магчыма стварыць асінхронную версію reduce, якая будзе працаваць правільна, але яна часта є складной для розумэння, адколі сам аккумулятор являе сабою праміс, які трэба астаточна распакаваць у кожной ітерацыі.

Калі гэта стане ясным, напісанне правильнага коду перестае быть падтрымкам запам’ятоввання, які метод масі є „дазволеным“, і стае падтрымкам выбору модэлі выканання, якая на самай працы трэбуецца. Калі кожны крок практычна залежыць ад завершэння пярэднего, цікл for...of з await усередзіне прымусова выражае гэты намер. Калі крокі ўзаемна незалежныя і можаць выконванычы адночасова, ўпорядкуванне іх у масу прагненняў і чаканне ўсіх іх за дапамогою Promise.all часта ёсць правым рашэнням. А калі трэба обмежыць паралельнае выканання, ні просты цікл, ні необмежаны Promise.all не справяцца з задачай.

Сынтаксіс должен вылічвацца на адной ступені з аперацыйнымі патрэбамі, а не наадворс. Існуе спакуса выбраць той патэрн, які здаецца ідыяматычным, і спадзявацца, што жадана працэздатнасць выйдзе з гэтаго, але такі порядак дзейства є неправым.

Прытлівасць Promise.all без рызыка?

Пасля таго, як стало вядома, што forEach ніколи не чакае, наступны логічны крок — апытка викорыстаць Promise.all, які зазвычай ўзяты за правильны інструмент: перетворыць элементы на асінхронныя операцыі, передаць отрыманыя прамесы ў Promise.all і чакаць, пакуль усе будзе завершана адночасна.

Этот падход ўсё працюе, калі аднальныя операцыі не залежаць адзінадзе з іншым, розмер пакета застаецца разумным, і адна нявыполненая операцыя мае знечысціць усё рэзультат. Проблэмы пачынаюцца, калі яго викорыстоўваюць за межамі гэтых умов.

Promise.all сама пацэльна нічога. Большая частка адпрацоўкі пачынаецца ў той момент, калі ствараюцца праграмы, таму обработка калькі з адзінакоў тысяча записаў може запусціць калькі з адзінакоў тысяча запитоў да базы дадзенаў або зворотных запросаў практычна адночасна. Такі код можа выглядаць нормальным для маленькага локальнага набора дадзенаў, але пасля працы з великімі данымі ён може перапрацаваць пулы з’ўязкаў, перасягнуць ліміты частоты запытоў або выкарыстаць усю памяць.

Яго обработка адказоў таксама може быць неспадзеванай. Калі адна з праграм у групе выклікае памылку, рэшта не зупіняюцца автаматычна. Яны продовжаюць працаваць, тое значыць, яны ўсё ўсё можаўць запісваць записы, адправляць паведамленні або змянюваць стан зовнішніх систем, нават пасля таго, як код-заклекчык уже перайшоў у блок catch. Заява "пакет не выйшаў" тэхнічна ёсць правядомай, але гэта не значыць, што не было жадных наследків.

Практыка павтарэння выканання цэлага аб’екта пакета пазней можа зноў выконваць тыя задачы, якія вже былі завершаны пад час першага спробу. У такім случае прычына не ў механізмах promise — яна крыўцяе ў ідэмпотентнасі та способах абрабаткі частковага завершэння.

Перш чым апытацца з Promise.all, корыстна задаць іншыя запитанні. Скількі операцый на самай працоўнае можна выканаваць адразу? Чы справды яны незалежныя адзін ад другога? Што мае значыць адна неудача для рэшты пакета? Чы існуе спосаб зупініць выканання, яке вже пачалося? Якшы адбудзецца павтарэння, чы могуць айтэмы, якія вже былі завершаны, стаць дублюючыміся? Чы апытальнік практычна патрэбуе кожны результат, чы можа будзе прыйнятна частковая успех?

Інодзе Promise.all застаёцца правым рашэнням. Інодзе Promise.allSettled падходзіць лепей. Іншыя разы ситуацыя вымагае последовнага циклу, лімітара канкурэнцыі, або очаквальнай лініі, якая абгрупавае весь процес. Тое, калі яны ўсё-такі правыя, залежыць ад гарантый, якія павінен даставаць процес, а не ад таго, насколькі шыбка ўвесь код здаецца на першы погляд.

Прыпуск, што код, які чытаецца зверху вніз, не можа стварыць ситуацый канкурэнціі

Можа, найболей дорогія з усіх непарэнняў — гадка, што await заходзіць код ад ситуацый канкурэнціі. У ўсероўнай функцыі запаведзі пасля await продовжаюць выконваліцца па порядку, таму функцыя здаецца адной цэлай последовнасцю. Але праз канкурэнтныя вызывы той самай функцыі калькі екзекуцыйяў можа легка перакрывацца.

Уявіце два запиты, кожны з якіх чытае потачку, вырахоўвае новую потачку і зберагае яе. І ты, і іншы запит чакаюць на завершэння чытання, а таксама чакаюць на завершэння запісу. Кожны крок у межах кожнага запиту выконваецца у той порядак, які мы моглі б спадзявацца. Аднак ситуацыя конкурэнціі все равно виникае, таму што оба запиты можу чытаць тую ж самую пачатковую потачку, прычаму ніхто з іх яшчэ не запісаў свою адкоректаваную версію.

Такой жа тип бага з’яўляецца на фронтэндзе. Спачатку выконуецца пошук за старым запитам, а пасля — другі пошук за новым. Новы запит випадкова завершуецца першым і правільна адкоректавае інтерфейс. Стары запит завершуецца пасля і перезапісвае гэты правільны стан застарелым резултатам. Кожны запит чакаў на свой фетч ровна так, як і планавалася — адсутня толькі правіла, якія б вказвалі, який запит ўсё ще мае право адкоректаваць інтерфейс.

Эта прамова ўважліва для кожнага await, які знаходзится межы чытання стану і выконання дзеяння з яго. Пауза — гэта не проста безнебяжная перыяд часу. Це вакансія, калі можа выконвацца іншая робота, якая можа знеасаблівати прыпускі, якія функцыя зробіла непразь до паузы.

Перакананні, якія выконвуцца на рэўэльнай ступені, не захоўваюцца автаматычна пра гэты перыяд часу. Перакананне, што логін вялікі, і яго падключэнне не даюць захоплення ад двух запытак, якія выкарыстоўваюць тое ж самае перакананне практычна адночасна. Перакананне, што запис яшчэ чакае на затверджэнне, не гарантуе, што іншы процес не змяніў яго стан у межах гэтага часу.

Рэальная захаўненасць зазвычай трэба атрымваць з нижэйшага рывня: правіла унікальнасці на рэвэлю базы дадзеных, умовная апдэйтаванне, оптымістычнае блакаванне, ключ ідэмпотэнцыі, транзакцыя або часткавы правіла прыналежнасці, якія выкарыстоўваюцца ў інтэрфейсе. await керуе толькі завершэнням адной конкрэтной обяцанні. Ён нічога не робіць для блакавання спяльнага стану або для гарантавання таго, што значэнне, прынятыя раней, застанецца правдзівым па час выканання дзеяння.

Аспакоўкі, што try/catch падхопіць задачу, якая ніколі не была await-авана

Іншая частая памылка выглядае абсалютна безпечной пад час перагледу коду. Асінхронны вызов знаходзіцца ў блоке try/catch, і здаецца логічным прыпускати, што будзь-яе адхіленне будзе обрабатавана там.

try {
  sendAnalyticsEvent(event);
} catch (error) {
  logError(error);
}

Калі sendAnalyticsEvent ўжо є асінхронным функцыяй, якай адхоўляеся пасля таго, як восьбіць свойа запаведзіння, блок catch, які знаходзится ўсереды, ніколі не бачыць гэтага адхоўлення. Сінхронная частка вызову успелася ў той момент, калі вона восьбіць запаведзіння. Адхоўленне вырабляецца пасля таго, за межамі стак-фрэма, які стежыць блок try.

Блок catch працюе толькі тады, калі запаведзіння чакаецца ўсереды яго:

try {
  await sendAnalyticsEvent(event);
} catch (error) {
  logError(error);
}

Якща сказаць проста, разлік здаецца явным, але пачатковая версія здаецца безпечной самэй таму, што асінхронны вызов візуальна знаходзится ўсереды працэсара адлучэнняяў. Такая структура создае вражанне наявнасці зв’язку, якога код на самай працэ практычна не стварае.

Той жа проблема выклікаецца ўнутрь калбэкаў і працоўнікаў змаганняў. Асінхронны калбэк можа адмовіцца ўнутршна, але якщо код, які яго вызывае, ніколі не чакае і не пераглядае прапазу, якую ён вяртае, то гэта адмова не мае куды пайсці. Абгортаванне месца вызыву ў try/catch не дапамагае, таму што адмова належыць да прапазу, якую нічога ў межах дыапазону не стежыць.

Основныя правілы такія: адказы ў асінхронным кодзе перавозяцца разам з прапазу, а не за дапамогою індэнтавання. Чыгунуванне адмовы выклікаецца шляхом прымэння прапазу безпосередна, ўвозкі яе ў ланцюг, які вялічыць працоўніка, або падключэння явнага калбэка для адмовы. Як толькі прапаза зброшуецца, ўсё, што было пов’язана з яе адказамі, таксама зброшуецца.

Гэта таксама мае значэнне для блакоў падхоплення, якія працуюць занадта інтэнсыўна. Ператвараць кожную неудачу на значэнне null стварае адзін зановы проблем: тыя, хто вызывае код, больш не можу разлічыць справжню неудачу ад законамерна порожняга рэзультата. Проста падхопіць адказ на адказ не ёсць метай. Код павінен захаваць значэнне той неудачы для таго, хто яго будзе выкарыстоўваць далей.

Плутанне анулявання з перакрыцчам

Калі команда пачынае выкарыстоўваць AbortController, бывае спакуса прыпускать, што ануляванне запиту означае, што фактычна завершылася вся работа. Для запитоў, выданых з браузера, ануляванне дэйсна не дазволяе кліенту продаваць чакаць, і часта запобiegае непатрэбнай дадатковай обработцы на самам кліенте. Гэта справдзіва корыстна для такіх речаў, як жывой пошук, переход з адной стороніцы на іншую чы выдаленне некалькіх дадзенаў, якія больш не є актуальнымі.

Што гэта не гарантуе, так то тое, што работа, яка ўжо перадана серверу, будзе анульвана.

Якщо запит на зберагчыць данні абарануецца пасля таго, як сервер яго вже прыняў, бэкенд можа продаваць завершэнне апдэйта базы дадзеных чы выкананне зову на званіцу, незалежна ад гэтага. Кліент фактычна ведае толькі тое, што ён перестаў чакаць адпаведзення. Прабава падаць той жа запит знову без якога-небудзь захавання ідэмпотентнасці стварае рызык павтарэння аператыў, якія ўжо успелі на сервере.

Гэта разлік існуе таму, што абаранаванне з боку кліента і стан сервера знаходзяцца на протылежных бакох мерага. Кліент можа відмовіцца ад прагнення да рэзультата, але у яго няма можлівасці перацягнуцься через гэты мераг і анульваць эфекты, якія ўжо выйшлі на практыку.

Анулюванне лепша спрыяе розумеўцю значэння результата, чым ёсць гарантія яго стану. Яно паведамляе рэшту прыемлівача, што аберучы не заінтэресаваны ў конкрэтным результате, таму гэты результат не павінен перазапісваць ныяны стан. Для операцый чытання абаранаванне запиту ёсць разумной оптымізацыяй. Для запісу все ж трэба окремы механізм, каб з’ясаваць, чыи операция завершылася, яшчэ трывае чы можа быць прабавана знову без дуплікацыі яе эфекту.

Анулюванне касаецца таго, чы хочацца ўсё ж результата. Яно не касаецца таго, чы падстаўная дзеянне ўсё ж застаецца супараднальным.

Запіс асінхронной сынтаксі прыяўнае да визначэння рабочага прайсепту

Адыякавальны момант у всіх этых памылак — гэта спрыяванне асінхроннага дизайна як да чыста справы сінтаксісу, калі спачатку падумваецца пра вжытак await, Promise.all чы ў асінхронным калебэку, перш чым з’ясаваць, якія гарантіі на самай працэйнае падазроўваецца.

Болей корыстныя запытанні задаюцца раней. Чы трэба таму, хто вызывае, чакаць, пакуль гэта завершыцца, прычымляючыся да наступнага? Чы можна без апасцяры бегаць калькі задач адночасова? Чы мае значэння ўзаемная парада іх выканання? Какі ўзьязд, калі якія з іх успехоўцяюць, а іншыя — ні? Чы безпечна прабаваць выканаць гэтую працэю знову? Хто несе адпаведнасць за раскідкі? Чы стары рэзультат можа ўсё ж такі заменіць спяльны стан? І калі ў чымсь выйдзе тайма-аут, чы гэта значыць, што працэя зазнала невузятка, чы толькі што таму, хто яе вызываў, перастаў чакаць?

Калі на этыя запытанні ўжо будзе дана адпаведная адказ, сам JavaScript зазвычай працуе без значных трудносцэй.

У ситуацыях, дзе порядак практычна мае значэнне, можна выкарыстоўваць цыкл, які по чарга адбывае кожны крок. Незалежныя задачы з вядомым, обмежаным колькістю выкарыстоўваюцца паралельна. Большыя пакеты можна обрабоцаваць з абмежэнням паралелізма. Роботу, якая не павінна блакаваць вызывача, можна перадаць у стойкую канэўку. Апдэйты спільнага стану можна рэгулюваць за дапамогою чысткіх пераканальнікаў прыналежнасці. Запісы ў базу дадзеных можна атамарна захаваць, выкарыстоўваючы механізмы інваріянтнасці на роўні зберагання. У операцыях, дзе наявнасць дубліката мае великія наследкі, можна выкарыстоўваць ключы ідэмпотентнасці, южабныя тое, каб праказа не стварыла другую копію таго ж рэзультата.

Складнасць заключаецца не ў чым іншым, як у правильным размешчэнні оператара await. Неабходна з’ясавіць, што на самай працоўнай грані значыць тэрмін „завершанне“.

Знаюць ключавое слова, але не контракт

Неправильна ўпэўненасць у викорыстоўванні async/await працоўнае гады стосуецца меньш таго, што людзі забываюць, як працуе сынтаксіс, а больш таго, што гэта робіць асінхронны код выглядзе наблыжэй до секвенцыйнага, локальнага і прыгадныя, чым ён на самай працоўнае є.

Відразу ж пасуюць мысль, што await значыць, i ўсё, што знаходзіцца яго або пад чым, таксама зупінілася. Лёгка адмаўляць можабальнасць вызвання асінхронной функцыі без чыткага вялікання, хто будзе адпаведальны за ўсуненне её працяў. Выкорыстоўванне сінхронных методаў масіва з асінхроннымі калбэкамі прыводзіць да таго, што яны спадзяюцца, iх адносінні будуць разумныя, хоця гэта не так. Адразу ж апылівацца Promise.all, не разважаючы пра навантажэнне чы і ў чаму будзе, калі толькі дзеяныя з прагнозаў будуць успешнымі, — гэта стандартная, але рызыкаваная прычына. Дазволяць коду працаваць зверху да ніжы, нават калі калькі можаць рэальна перакрывацца па часу, маскіруе рызыкі, якія ўсё ж існуюць. Спадзявацца, што try/catch пазберэ ад адхіленняў прагнозаў, якія ніколі не былі чаканыя, а таксама плутаць анульаваны запит з доказам таго, што на сервере нічога не выйшла, — гэта ўсё наступак з той самый слепы пункт.

Кожна з эых памылак выклікаецца з той самай прасоўкі: прасоўкі межы таго, як выглядае код, і таго, чаму ён насправды обяцвае.

Правильнае чытанне асінхроннага коду значыць выявляць обяцанні, якія існуюць за межамі функцый, а не проста чытаць яго зверху вярху. Це таксама значыць пераканацца, хто іх стварае, хто на яны чакае, хто адпавядае за обработку адхіленняў, і яка частка стану все ўсё ж мае права на рэзультат пасля таго, як усё стане ясна. Це таксама значыць прыдзельваць увагу кожнам моменту, калі await стварае паўзу, адтолькі ў самай гэтай паўзе іншыя задачы можу зменіць прыпускі, на якіх спакоювалася функцыя.

Код на сторанцы все ўсё можа выглядаць секвэнцыйным. Але такі выгляд больш не значыць, што система будзе дзейсніцца так сама.

Гэтыя змены ператвараюць асінхронны JavaScript з набору ключоўых словаў у модэль, падбудаваную на прынцыпах власнасці, часавага планування і распакоўкі асінхронных задач. Гэта таксама поясняе, чаму код, які годзінамі выглядаў правільным, можа ўсё ж паспелаць у продакшэне, незважаючы на тое, што праходзіў усе тэсты.

Навучыцца чакаць на promise — гэта простая частка.

Розумець, што робітвае рэшта программы пад час гэтага чакання, трэба значна дольшы час.

Спаднёе чытанне

  • Пашчотныя прынцыпы JavaScript і TypeScript, якія таямна ламаюць код — Апаведае пра тонкія прынцыпы JavaScript і TypeScript, такія як парадаксы з NaN, проблемы з асінхронным часавым планам і прымусовая змена типу, якія вызываюць багі, нават калі код выглядае правільна.
  • Пашчотныя уявленні пра Node.js і базах дадзеных, якія вызываюць багі ў прыменэннях — Дазволяе зразумець, чаму асінхронныя методы async/await, пулы з’язоў і ORMs не запобегаюць автаматычна супернаганянню, выкаранню з’язоў чы інжэкцыі SQL у прыменэннях на Node.js.
  • Дзевяць шаблонаў Promise для надзеянага асінхроннага JavaScript у прымэтнае выкарыстоўванне — Вы дазнаецеся пра практычныя шаблоны Promise — паралельныя запиты, таймауты, павторныя спробы, ліміты канкуранцыі і анулювання — для стварэння стойкага, прымэтнае-гатовага асінхроннага JavaScript.
  • Усуненне бягаў у обробцы адзінакоў у async/Await у коде Node.js для прымэтнае выкарыстоўванне — Вы дазнаецеся пра пяць распашчытых адзінакоў у обробцы адзінакоў у async/await у JavaScript і Node.js, якія спрычыняюць тыхія адзінакі і ситуацыі канкуранцыі, а таксама пра конкрэтныя способы ўсунення яных.
  • Сэмь распаўсюджаных ідіумаў JavaScript, якія таямніча прыносяць будучыя багі — Адказвае на пытанне, як такі ўжываныя ў кожны дзень патэрны JavaScript, такія як перакананне ў правдзівасці, неабавязковая ланцоўка і синтаксі распрасцёру, маскуюць заўважэнняя, якія таямніча вылучаюцься праз развіцце коду.