Error handling async'da
Nega oddiy try/catch asinxron kodda ishlamasligi, Promise'da xato .catch() orqali qanday tarqalishi, async/await try/catchni qanday 'tiklashi', Promise.all/allSettled/race/any orasidagi xato boshqarish farqi, va unhandled rejection nima ekani.
51-dars
Error handling async'da
1. Muammoning ildizi — nega oddiy try/catch yetarli emas
Sinxron kodda xato boshqarish oddiy — throw qilingan zahoti, uni o'rab turgan try/catch ushlab oladi:
try {
throw new Error('xato');
} catch (e) {
console.log('ushlandi:', e.message); // ishlaydi
}
Lekin asinxron kodda muammo bor:
try {
setTimeout(() => {
throw new Error('xato'); // BU YERDA throw qilinsa nima bo'ladi?
}, 1000);
} catch (e) {
console.log('ushlandi:', e.message); // BU HECH QACHON ISHLAMAYDI!
}
Nega ishlamaydi? Sababi — Call Stack tarixini eslang: try bloki darhol bajariladi va tugaydi (setTimeoutning o'zi — callback'ni Web API'ga "topshiradi" xolos). 1000ms dan keyin, callback yangi, alohida Call Stack bilan ishga tushadi (Event Loop orqali) — bu paytda eski try/catch allaqachon Call Stack'dan chiqib ketgan, mavjud emas.
throw — bu yangi, "yalang'och" Call Stack'da sodir bo'ladi, uni ushlab oladigan hech narsa yo'q → dastur (yoki kamida shu callback) crash bo'ladi.
try {} — Call Stack #1 (tez tugaydi, catch bilan birga yo'qoladi)
│
▼ 1000ms keyin
callback() — Call Stack #2 (BUTUNLAY YANGI, eski try/catch'dan XABARSIZ)
Bu — asosiy sabab, nega Promise va async/await maxsus xato boshqarish mexanizmiga muhtoj.
2. Promise'da xato — .catch() orqali
Promise — bu muammoni holat (state) orqali hal qiladi (oldingi mavzuda ko'rganimiz [[PromiseState]]: rejected). throw qilingan xato — Call Stack orqali emas, balki Promise obyekti ichida saqlanadi va keyinroq .catch() orqali "olib chiqiladi":
function getData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error('server xatosi'));
}, 1000);
});
}
getData().catch((err) => console.log('ushlandi:', err.message));
Bu ishlaydi, chunki reject(err) — Call Stack orqali emas, balki Promise'ning [[PromiseResult]]iga yozadi, so'ng .catch() (aslida .then(undefined, onRejected)) — bu qiymatni Microtask sifatida, keyinroq, alohida o'qiydi.
Executor ichida throw — avtomatik rejectga aylanadi. Muhim nuance: agar Promise executori ichida (sinxron qismida) xato tashlansa, u avtomatik reject deb hisoblanadi:
new Promise((resolve, reject) => {
throw new Error('sinxron xato'); // reject(new Error(...)) bilan BIR XIL
}).catch((err) => console.log(err.message)); // "sinxron xato"
Lekin bu faqat executor ichida ishlaydi — agar throw asinxron callback ichida (masalan, setTimeout ichida) bo'lsa, bu qoida ishlamaydi, chunki o'sha yerda Promise mexanizmi bilan hech qanday bog'liqlik yo'q:
new Promise((resolve, reject) => {
setTimeout(() => {
throw new Error('bu ushlanmaydi!'); // Promise buni "ko'rmaydi"
}, 1000);
}).catch((err) => console.log(err)); // HECH QACHON chaqirilmaydi
To'g'ri yozish — setTimeout ichida rejectni qo'lda chaqirish kerak (yuqoridagi misoldagidek).
3. async/await bilan — try/catch QAYTA ISHLAYDI!
Bu — async/awaitning eng katta afzalliklaridan biri: chunki await ichkarida Promise'ning rejected holatini throwga aylantirib beradi, oddiy sinxron try/catch yana ishlay boshlaydi:
async function run() {
try {
const data = await getData(); // getData() reject bo'lsa...
console.log(data);
} catch (err) {
console.log('ushlandi:', err.message); // ...BU YERDA ushlanadi!
}
}
Nega bu ishlaydi, sinxron setTimeout misoli esa ishlamagan edi? Oldingi Async/Await mavzusida ko'rganimizni eslang: await — funksiyani muzlatadi, lekin funksiya (uning generator-o'xshash "davom etish nuqtasi" bilan birga) saqlanib qoladi. Promise rejected bo'lganda, V8 ichki mexanizmi bu funksiyani xuddi o'sha try bloki ichidan davom ettiradi — va reject qilingan qiymatni throw qilingan xato sifatida generator ichiga "otadi" (generator.throw(error) — oldingi mavzuda ko'rgan runAsync misolidagi aynan shu qator).
await getData()
│
▼ getData() ichidagi Promise "rejected" bo'ldi
│
▼ V8: generator.throw(error) chaqiriladi
│
▼ bu — xuddi shu joyda "throw error" yozilgandek ishlaydi
│
▼ funksiya ICHIDAGI try/catch buni ODDIY throw sifatida ushlaydi
Shuning uchun async/await — Promise'ning asinxron xato boshqaruvini, sinxron try/catch sintaksisi bilan "ko'rinadigan" qilib beradi — bu uning eng katta amaliy foydasi.
async funksiyada throw — avtomatik rejectga aylanadi. Aksincha yo'nalish ham ishlaydi — async funksiya ichida oddiy throw yozsangiz, u qaytargan Promise avtomatik rejected bo'ladi (buni oldingi mavzuda ko'rgan edik):
async function run() {
throw new Error('xato');
}
run().catch((err) => console.log(err.message)); // "xato"
Bir nechta await — bittasi xato bersa, qolganlari bajarilmaydi:
async function process() {
try {
const a = await fetchA(); // muvaffaqiyatli
const b = await fetchB(); // BU YERDA xato bo'lsa...
const c = await fetchC(); // ...BU UMUMAN chaqirilmaydi
} catch (err) {
console.log('qayerdadir xato:', err.message);
}
}
Bu — oddiy try/catch xatti-harakati bilan bir xil: xato tashlangan zahoti, try blokining qolgan qismi o'tkazib yuboriladi, boshqaruv darhol catchga sakraydi.
4. Promise.all() — "hammasi yoki hech narsa"
Bir nechta Promise'ni parallel kutayotganda, xato boshqarish qanday ishlashi muhim farq qiladi:
Promise.all([fetchA(), fetchB(), fetchC()])
.then(([a, b, c]) => console.log(a, b, c))
.catch((err) => console.log('BIRINCHI xato:', err.message));
Promise.all() — agar ro'yxatdagi bitta Promise ham reject bo'lsa, butun Promise.all() darhol reject bo'ladi — qolgan Promise'lar hali pending bo'lsa ham (ular fonda ishlashda davom etadi, lekin natijalari e'tiborga olinmaydi):
fetchA() — 1s da fulfilled
fetchB() — 2s da REJECTED
fetchC() — 5s da fulfilled (lekin bu natija KUTILMAYDI)
Promise.all natijasi: 2s da, fetchB xatosi bilan REJECT bo'ladi
Bu — ba'zan noqulay: agar siz "qaysi so'rovlar muvaffaqiyatli, qaysilari xato bergani"ni bilishni xohlasangiz, Promise.all() yetarli emas.
5. Promise.allSettled() — hech qachon reject bo'lmaydi
const results = await Promise.allSettled([fetchA(), fetchB(), fetchC()]);
results.forEach((result) => {
if (result.status === 'fulfilled') {
console.log('muvaffaqiyat:', result.value);
} else {
console.log('xato:', result.reason);
}
});
allSettled — har bir Promise tugashini kutadi (muvaffaqiyat yoki xato — farqi yo'q), va natijada hech qachon o'zi reject bo'lmaydi — u har doim {status, value} yoki {status, reason} obyektlari massivini qaytaradi. Bu — bir nechta bir-biriga bog'liq bo'lmagan so'rovni yuborib, hammasining natijasini (xato bo'lsa ham) yig'ib olish kerak bo'lganda ishlatiladi.
6. Promise.race() va Promise.any() — qisqacha farq
Promise.race([p1, p2, p3])
→ BIRINCHI hal bo'lgan Promise natijasini qaytaradi
(fulfilled BO'LSIN, rejected BO'LSIN — farqi yo'q, birinchisi g'olib)
Promise.any([p1, p2, p3])
→ BIRINCHI FULFILLED bo'lgan Promise natijasini qaytaradi
(agar HAMMASI reject bo'lsa — AggregateError qaytaradi,
bitta muvaffaqiyat yetarli, xatolar e'tiborga olinmaydi)
7. Unhandled Promise Rejection — "yutilib ketgan" xato
Eng xavfli holat — Promise reject bo'lgan, lekin hech qanday .catch() yoki try/catch uni ushlamagan:
async function risky() {
throw new Error('hech kim ushlamaydi');
}
risky(); // .catch() YO'Q, await ham YO'Q
Bu holatda dastur crash bo'lmaydi (Promise'lar sinxron throwdan farqli, dasturni to'xtatmaydi), lekin brauzerda konsolga "Uncaught (in promise) Error" ogohlantirishi chiqadi, Node.js'da esa (yangi versiyalarda) bu process'ni butunlay to'xtatishi mumkin.
V8 darajasida qanday aniqlanadi? Oldingi mavzuda ko'rgan [[PromiseIsHandled]] internal slot'ni eslang — Promise reject bo'lganda, agar unga hech qachon .catch() yoki .then(_, onRejected) ulanmagan bo'lsa, bu flag false bo'lib qoladi. V8/runtime shu holatni maxsus kuzatib boradi (bir microtask tsikli oxirigacha), va agar hali ham false bo'lsa — unhandledrejection event'ini (brauzerda) yoki xatoni (Node.js'da) chiqaradi.
window.addEventListener('unhandledrejection', (event) => {
console.log('Ushlanmagan xato:', event.reason);
event.preventDefault(); // brauzer konsoliga chiqishini to'xtatadi
});
Xulosa — nima uchun xato "yo'qolib ketishi" mumkin
Amaliyotda eng ko'p uchraydigan xato — awaitni unutish:
async function bad() {
getData(); // await YO'Q! Bu Promise'ni "otib yuboradi", natija ham, xato ham yo'qoladi
console.log('davom etdi'); // getData tugashini kutmasdan, darhol ishga tushadi
}
Bu yerda getData() — Promise qaytaradi, lekin unga hech kim (await ham, .catch() ham) "quloq solmaydi" — agar u reject bo'lsa, bu unhandled rejection bo'lib qoladi, va funksiyaning qolgan qismi bu haqda hech qachon bilmaydi.
Sinxron xato → Call Stack orqali "portlaydi", darhol ko'rinadi
Promise xato → [[PromiseState]]da "yashiringan", faqat .catch()/await
orqali "chiqariladi" — ushlanmasa, jimgina yo'qoladi
(faqat konsol ogohlantirishi bilan)
Aynan shuning uchun har doim await ishlatilgan joyda try/catch (yoki funksiya darajasida umumiy .catch()), va fire-and-forget Promise'lar uchun ham kamida .catch(console.error) yozish — professional amaliyotning muhim qoidasi hisoblanadi.