Sintaksi ya async/await ya JavaScript ilikusudiwa kutuokoa kutoka kwenye "callback hell". Badala yake, ilileta tatizo tulivu zaidi na la hatari zaidi: kodi inayonekana sahihi lakini inafanya kazi isiyotabirika. Unaona await ikiwa hapo kwenye mwili wa kazi (function body) na unadhani kila kitu kinasimama kwa utulivu, mstari kwa mstari. Mara nyingi haifanyi hivyo. Mizunguko (loops) inakimbia mbele. Makundi yote ya kazi (batches) yanaporomoka kwa sababu ya ombi moja lililofeli. Faili za kuingilia (entry files) zinaanza kujaa viunganishi (wrappers) vya async visivyo na sababu. Ikiwa umekutana na mojawapo ya haya, mifumo hii mitatu itayarekebisha.
Acha Kutumia await Ndani ya forEach
Hapa kuna kosa la kawaida ambalo linaonekana kutokuwa na madhara kwenye ukurasa:
const urls = ['/api/user', '/api/posts', '/api/comments'];
urls.forEach(async (url) => {
const res = await fetch(url);
const data = await res.json();
console.log(data);
});
console.log('All done!');
Ikiendeshwa, 'All done!' itachapishwa kabla hata ya jibu moja kurudi. Kwa nini? forEach inatekeleza callback kwa kila kipengele mara moja. Haiangalii ahadi (promise) iliyo ndani ya kila mzunguko (iteration). Neno async linageuza kila callback kuwa promise ambayo forEach inaitupia kando mara moja. Mzunguko wako unamalizika ndani ya microsecond chache; maombi ya mtandao yanaendelea peke yake. Ikiwa unahitaji makosa (errors) yashughulikiwe kwa mpangilio, au ikiwa unahitaji kuhakikisha ombi moja linamalizika kabla ya lingine kuanza, mfumo huu unavunja ahadi zote mbili kimyakimya.
Ibadilishe kwa kutumia mzunguko wa for...of:
const urls = ['/api/user', '/api/posts', '/api/comments'];
for (const url of urls) {
const res = await fetch(url);
const data = await res.json();
console.log(data);
}
console.log('All done!');
Sasa mzunguko unasimama kweli kwenye kila await. Ombi la pili linasubiri la kwanza. 'All done!' inachapishwa baada tu ya kila kitu kukamilika.
Tumia for...of wakati mpangilio ni muhimu—kupakia faili moja baada ya nyingine ili kuheshimu mipaka ya kasi (rate limits), kuandika mistari ya hifadhidata (database rows) kwa mpangilio maalum, au kuunganisha (chaining) wito wa API ambapo ombi linalofuata linahitaji data kutoka kwenye jibu lililopita. Ikiwa unataka utekelezaji wa sambamba (parallel execution), usijaribu kutumia forEach. Tumia Promise.all waziwazi ili nia yako ionekane kwa msimamizi wa kodi anayefuata. Lakini usichanganye await na forEach ukitarajia tabia ya synchronous. Haitatokea.
Tumia Promise.allSettled Wakati "Sifuri" Haiwezi Kuwa Jibu
Promise.all inaaminika kimaana. Ipe array ya promises, na itakurudishia array ya matokeo. Changamoto ni kwamba wakati wowote promise moja inapokataliwa (rejects), kitu chote kinakataliwa mara moja. Kila promise nyingine inayosubiri inaachwa kumalizika peke yake, lakini unapoteza ufikiaji wa matokeo yake. Katika mazingira ya uzalishaji (production), tabia hii ya "kila kitu au hakuna kitu" inaumiza.
Wazia programu yako inavyopata widget za dashboard kutoka kwa huduma nne huru: uchambuzi wa trafiki, data ya mapato, maoni ya watumiaji, na afya ya seva. API ya mapato inafikia muda wake wa mwisho (timeout) kwa muda mfupi. Chini ya Promise.all, dashboard yako nzima inatoa kosa (error). Majibu matatu yenye afya yanapotea. Mtumiaji anaona spinner, kisha skrini ya hitilafu, kwa sababu robo moja ya data imefanya vibaya.
Promise.allSettled inakupa mkataba wenye akili zaidi. Inasubiri hadi kila promise imalizike, iwe imefanikiwa au imefeli. Thamani iliyopatikana (resolved value) ni array ya objects zinazoelezea kila matokeo:
const requests = [
fetch('/api/traffic'),
fetch('/api/revenue'),
fetch('/api/feedback'),
fetch('/api/health')
];
const results = await Promise.allSettled(requests);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
renderWidget(index, result.value);
} else {
renderError(index, result.reason);
}
});
Hakuna jibu linalotupwa. Unatoa (render) kile unachoweza na kutenga hitilafu. Mfumo huu ni muhimu kila unaposhughulika na operesheni zisizohusiana—arifa za pamoja (bulk notifications), utumaji wa webhook kutoka kwa upande wa tatu, au kuingiza rekodi kutoka kwa vyanzo vingi vya CSV. Bado unahitaji ufuatiliaji wa makosa uliowekwa katikati, lakini programu yako inaendelea kusimama imara.
Kumbuka moja la vitendo: allSettled inarudisha seti nzima, kwa hivyo bado unahitaji kuchuja matokeo na kuamua nini maana ya "mafanikio ya sehemu" (partial success) kwa kipengele chako. Usichukulie array iliyorudishwa kama data yenye mafanikio yote. Angalia zile sehemu za hali (status fields) kabla ya kuingiza kitu chochote kwenye tabaka lako la hali (state layer).
Tangaza Top-Level await na Ondoa Wrapper ya IIFE
Kwa miaka mingi, ikiwa unataka kusubiri (await) kitu kwenye mzizi (root) wa faili, ulikuwa unalifunga ndani ya async function inayozinduliwa mara moja (immediately invoked async function):
(async () => {
const config = await loadConfig();
startServer(config);
})();
Hii inafanya kazi, lakini ni kelele tu. Top-level await, ambayo ni asili katika ES modules, inakuwezesha kuacha kodi ya ziada ya kisherehe (ceremonial boilerplate):
const config = await loadConfig();
startServer(config);
Tumia hii kwenye nukta ya kuingilia (entry point) ya programu yako au kwenye moduli maalum za usanidi (configuration modules) ambapo uanzishaji (initialization) lazima ukamilike kabla ya kitu kingine chochote kufanya kazi. Kupakia faili za mazingira (environment files), kuanzisha pool ya muunganisho wa hifadhidata, au kupata bendera za vipengele (feature flags) za mbali ni mambo yanayofaa kabisa. Kwa sababu top-level await inazuia utekelezaji wa mchoro wa moduli (module graph)—faili nyingine zinazoimport hii zitasubiri promise yako imalizike—unapata hali iliyohakikishwa. Sehemu nyingine ya kodi yako inaweza ku-import db na kujua kuwa muunganisho tayari upo hai.
Kuna vikwazo viwili. Kwanza, runtime au bundler yako lazima iunge mkono ES modules. Katika Node.js, hiyo inamaanisha ama kutumia viambishi tamati vya .mjs au kuweka "type": "module" kwenye package.json yako. Pili, kwa sababu kusubiri katika kiwango cha moduli (module level) huchelewesha kila importer, fanya kazi inayosubiriwa (awaited work) iwe mahususi. Upataji mzito wa mfululizo (heavy sequential fetches) juu ya faili la utility linaloimportwa mara kwa mara utachelewesha mwanzo wa programu yako (cold start). Weka top-level await kwa ajili ya kazi halisi za bootstrap ambazo moduli nyingine zinategemea kweli.
Kinachobadilika Hali Halisi Unapochukua Mitindo Hii
Utabiri ni faida ya kwanza. Unaposoma loop ya for...of, unajua sawia wakati kitalu (block) kilicho chini kinapoisha. Hakuna ahadi za siri (ghost promises) zinazokimbia nyuma ya pazia, hakuna callback za foreach zinazojitenga na mishikaji wako wa makosa (error handlers). Mtiririko wako wa udhibiti (control flow) unaendana na umbo la kodi kwenye skrini.
Ustahimilivu unakuja baada ya hapo. Promise.allSettled inakulazimisha kufikiria kuhusu kushindwa kwa sehemu badala ya kutumaini kuwa kila mfumo wa nje utabaki kuwa mkamilifu. Programu za uzalishaji (Production software) si za aina mbili tu (binary). Baadhi ya maeneo ya mwisho (endpoints) yatakuwa na hitilafu. Baadhi ya usomaji wa faili utakumbana na makosa ya ruhusa. Kusanifu kwa ajili ya uhalisia wa hitilafu zilizotawanyika huifanya programu yako iendelee kufanya kazi bila kuficha data halali chini ya mkeka.
Uwazi huunganisha mambo haya. for...of inasomeka kama mfululizo wa Kiingereza cha kawaida. allSettled inaeleza nia yake katika jina lake. Top-level await huondoa viambishi vya IIFE visivyoeleweka ili faili zako za kuingilia (entry files) zianze na mantiki ya biashara (business logic) badala ya michezo ya kisintaksia (syntactic acrobatics). Mhandisi anayefuata atakayegusa faili hiyo—iwe ni wewe baada ya miezi sita au mwanachama wa timu aliye na muda mfupi—atakushukuru.
Funzo Halisi
Usichukulie async/await kama suluhisho la jumla ambalo unalieneza juu ya kodi iliyopo. Kagua miradi yako ya sasa kwa ajili ya mifumo hii mitatu mahususi ya anti-patterns. Tafuta await ndani ya kitalu cha forEach na ubadilishe kwa for...of au Promise.all ya makusudi. Pitia kila Promise.all inayozungumza na huduma za nje na uulize ikiwa kushindwa kwa kitu kimoja kunapaswa kweli kuharibu operesheni nzima; ikiwa sivyo, badilisha kuwa Promise.allSettled na ushughulikie matokeo mchanganyiko. Hatimaye, ondoa async IIFEs kwenye maeneo yako ya kuingilia ya ES module na uruhusu top-level await ishughulikie mfuatano wako wa bootstrap moja kwa moja. Hizi ni mabadiliko madogo ya kimekanika, lakini kwa pamoja yanageuza skripti zisizo imara za asynchronous kuwa kodi unayoweza kuiamini kweli.
