Series: JavaScript Basics Lesson 52

Promise chaining

Promise chaining'ning to'g'ri yozilishi: har doim return qilish qoidasi, ichma-ich .then() yozishning xatoligi, zanjir ichida qiymat transformatsiyasi, xatoning zanjir bo'ylab qanday oqishi, .finally(), va V8'da har bir .then() bo'g'ini qanday alohida Promise ekani.

52-dars

Promise chaining

Bu mavzuni oldingi Promise ichki mexanizmida ko'rgan narsalarga tayanib, endi amaliy tomondan — qanday to'g'ri yozish, qanday xatolarga yo'l qo'yilmasligi kerak nuqtai nazaridan ochamiz.

1. Oddiy ta'rif

Promise chaining — bir nechta .then()ni ketma-ket ulash, har biri oldingisining natijasi ustida ishlaydi:

fetchUser(1)
  .then((user) => fetchOrders(user.id))
  .then((orders) => fetchOrderDetails(orders[0].id))
  .then((details) => console.log(details));

Bu — Callback Hell'ning "tekislangan" (flat) versiyasi: ichma-ich emas, yon-baxsh, o'qish oson.


2. Asosiy qoida — HAR DOIM return qiling

Bu — Promise chaining'dagi eng ko'p uchraydigan xato manbai. Oldingi mavzuda (Promise ichki mexanizmi) ko'rganimiz Resolution Procedure'ni eslang: .then() ichidagi funksiya nima qaytarsa, keyingi .then() aynan o'shani oladi.

XATO — return unutilgan:

fetchUser(1)
  .then((user) => {
    fetchOrders(user.id); // ← return YO'Q!
  })
  .then((orders) => {
    console.log(orders); // undefined — chunki oldingi .then() hech narsa qaytarmadi!
  });

Bu yerda fetchOrders(user.id) — chaqiriladi, ish boshlanadi, lekin uning natijasi (Promise) hech kimga uzatilmaydi. Keyingi .then() — bu Promise haqida bilmaydi ham, u avvalgi .then()ning undefined qaytarganini ko'radi va darhol, fetchOrders hali tugamagan holda, ishga tushadi.

TO'G'RI — return bilan:

fetchUser(1)
  .then((user) => {
    return fetchOrders(user.id); // ← Promise QAYTARILDI
  })
  .then((orders) => {
    console.log(orders); // to'g'ri natija, fetchOrders TUGAGANDAN keyin
  });

Yoki qisqa arrow function bilan ({} yozilmasa, avtomatik return):

fetchUser(1)
  .then((user) => fetchOrders(user.id)) // bitta ifoda — avtomatik return
  .then((orders) => console.log(orders));

Diqqat: {} bilan yozsangiz, avtomatik return ishlamaydi — bu ko'p dasturchini "tutadigan" joy:

.then((user) => {
  fetchOrders(user.id); // { } ichida — return KERAK, avtomatik emas!
})

3. Nega chaining'da "flatten" bo'ladi — V8 mexanizmini eslatib o'tish

Oldingi mavzuda ko'rgan Promise Resolution Procedure aynan shu yerda ishga tushadi: .then() ichida Promise qaytarilsa, tashqi (.then() yaratgan) Promise o'sha ichki Promise hal bo'lguncha pendingda qoladi, keyin uning natijasini "meros qiladi":

.then((user) => fetchOrders(user.id))
        │
        ▼ fetchOrders() — YANGI Promise qaytaradi (masalan, p_inner)
        │
        ▼ tashqi .then() yaratgan Promise (p_outer) — HALI pending
        │   V8: p_inner'ga ICHKI .then() ulaydi
        │
        ▼ p_inner hal bo'lganda (masalan, orders massivi bilan)
        │
        ▼ p_outer HAM aynan shu natija bilan fulfilled bo'ladi
        │
        ▼ ZANJIRDAGI KEYINGI .then() — endi ishga tushadi, orders'ni oladi

Shu tufayli, nechta bosqich bo'lishidan qat'i nazar, .then().then().then()... — hech qachon Promise<Promise<Promise>>> bo'lib "chalkashib" qolmaydi.


4. Xato — ichma-ich .then() yozish (Chaining'ning noto'g'ri, "callback-uslubi" varianti)

// XATO — bu Callback Hell'ning Promise bilan qaytarilgan shakli!
fetchUser(1).then((user) => {
  fetchOrders(user.id).then((orders) => {
    fetchOrderDetails(orders[0].id).then((details) => {
      console.log(details); // 3 daraja chuqurlik — nega Promise ishlatdik?!
    });
  });
});

Bu texnik jihatdan ishlaydi, lekin Promise chaining'ning maqsadini yo'qqa chiqaradi. To'g'ri yo'l — har doim tekis (flat) zanjir, return orqali:

fetchUser(1)
  .then((user) => fetchOrders(user.id))
  .then((orders) => fetchOrderDetails(orders[0].id))
  .then((details) => console.log(details));

5. Zanjir ichida qiymatni "transformatsiya qilish"

.then() — Promise qaytarish shart emas, oddiy qiymat ham qaytarishi mumkin, u avtomatik keyingi bosqichga "sinxron ravishda" (lekin baribir microtask sifatida) uzatiladi:

fetchUser(1)
  .then((user) => user.id)          // oddiy qiymat qaytarilyapti
  .then((id) => id * 2)             // oldingisini qayta ishlaydi
  .then((doubled) => console.log(doubled));

Bu ikkalasi — Promise qaytarish (fetchOrders(...)) va oddiy qiymat qaytarish (user.id) — bir xil .then() interfeysi orqali ishlaydi, chunki Resolution Procedure ikkalasini ham qo'llab-quvvatlaydi (oldingi mavzuda ko'rgan "1-holat: oddiy qiymat, 2-holat: Promise" farqi).


6. Xato zanjir bo'ylab qanday "oqadi" — amaliy misol

fetchUser(1)
  .then((user) => {
    if (!user) throw new Error('foydalanuvchi topilmadi');
    return fetchOrders(user.id);
  })
  .then((orders) => {
    console.log(orders.length); // agar oldingi throw bo'lsa — BU O'TKAZIB YUBORILADI
    return orders;
  })
  .then((orders) => {
    console.log("yana o'tkaziladi"); // bu ham o'tkaziladi
  })
  .catch((err) => {
    console.log('faqat shu yerda ushlanadi:', err.message);
  });

Bu — oldingi mavzuda (Error handling) ko'rgan mexanizm: xato tashlangan zahoti, zanjirdagi barcha keyingi .then()lar (agar ularda ikkinchi argument — onRejected — bo'lmasa) avtomatik o'tkazib yuboriladi, faqat birinchi .catch() (yoki ikkinchi argumentli .then()) uni ushlaydi.

Muhim: .catch()dan keyin yana .then() qo'shsangiz, zanjir davom etadi — chunki .catch() ham yangi Promise qaytaradi, va agar xato ichida qayta throw qilinmasa, u fulfilled hisoblanadi:

fetchUser(1)
  .then((user) => { throw new Error('xato'); })
  .catch((err) => {
    console.log('ushlandi:', err.message);
    return 'zaxira qiymat'; // xatodan keyin ZANJIR DAVOM ETADI
  })
  .then((val) => console.log(val)); // "zaxira qiymat" — chunki catch xatoni "yopdi"

7. .finally() — natija/xatodan qat'i nazar bajariladigan qism

fetchData()
  .then((data) => console.log(data))
  .catch((err) => console.log(err))
  .finally(() => console.log('yuklanish tugadi')); // har doim ishga tushadi

.finally() — argumentga (natija yoki xato) kirish huquqiga ega emas, faqat "har qanday holatda ham shu ishni bajar" (masalan, loading spinner'ni yopish) uchun ishlatiladi. Ichkarida u — .then(onFinally, onFinally)ga o'xshab ishlaydi, lekin natijani/xatoni o'zgartirmasdan, zanjir bo'ylab shaffof o'tkazib yuboradi.


8. V8 darajasida — zanjirning har bir bo'g'ini nima

Har bir .then() chaqiruvi — Promise ichki mexanizmi mavzusida ko'rgan [[PromiseFulfillReactions]] / [[PromiseRejectReactions]] ro'yxatiga bitta yozuv qo'shadi, va yangi Promise obyekti yaratadi. Zanjir vizual jihatdan chiziq bo'lib ko'rinsa ham, xotirada bu — bir-biriga bog'langan, alohida-alohida Promise obyektlari zanjiri:

p0 (fetchUser natija Promise)
   │ .then() chaqirilganda:
   │   - p0.reactions.push(reaction1)
   │   - p1 = yangi Promise yaratiladi, QAYTARILADI
   ▼
p1 ──.then()──► p2 ──.then()──► p3 ──.catch()──► p4

Har bir bosqich — o'z navbatida, oldingisi fulfilled/rejected bo'lgach, alohida microtask sifatida ishga tushadi. Shuning uchun uzun zanjir (masalan, 5 ta .then()) — 5 ta ketma-ket microtask tickni anglatadi, hammasi bitta Macrotask ichida, lekin har biri alohida "sakrash" bilan (Event Loop mavzusida ko'rgan mexanizm asosida).


Xulosa — Chaining'ning uchta oltin qoidasi

  1. Har doim return qiling — aks holda keyingi bosqich sizning Promise natijangizni kutmaydi
  2. Ichma-ich yozmang — har bir .then() avvalgisidan Promise yoki qiymat qaytarsin, zanjir tekis bo'lsin
  3. Bitta .catch() — zanjir oxirida yetarli — u barcha oldingi bosqichlardagi xatolarni ushlaydi, har bir bosqichda alohida try/catch shart emas