IlmHamroh
JavaScript Full-stack/12-qism. JavaScript: ilg'or mavzular va kod sifati42/44-dars23 daqiqa
Mundarija (34)

Nozik xatolar: race condition, xotira sizishi, flaky test va Heisenbug

Qisqacha: Nozik xato — har safar emas, ba'zan chiqadigan xato: faqat sekin internetda, faqat oyning 31-kunida, faqat testlar boshqa tartibda ishlaganda. Asosiy turlari: race condition (natija ishlar qaysi tartibda tugashiga bog'liq), xotira sizishi (keraksiz narsa xotirada qoladi) va off-by-one (bittaga adashish). Yana: kasr son xatolari, vaqt zonasi va sana xatolari, flaky test (kod o'zgarmasa ham goh o'tadi, goh yiqiladi) va Heisenbug (kuzatmoqchi bo'lsangiz, o'zgaradi yoki yo'qoladi). Davolashning birinchi qadami doim bitta: xatoni har safar chiqadigan qilib takrorlash — tartibni, vaqtni yoki tasodifni o'zingiz boshqarib.

Bu darsda

  • "Ba'zan chiqadigan" xatolarning asosiy turlarini nomi bilan taniy olasiz.
  • Race condition'ni Node'da qayta yaratasiz, bajarilish tartibini qadamma-qadam ko'rasiz va navbat bilan tuzatasiz.
  • Flaky testni --test-randomize va soxta soat bilan har safar yiqiladigan qilasiz, keyin tuzatasiz.
  • Heisenbug nima ekanini va nega console.log qo'shganda xato yo'qolishi mumkinligini tushuntira olasiz.
  • Nozik xatoni qidirishning tartibli yo'lini qo'llaysiz: takrorlash, ajratish, sabab, test.

Oldin bilishingiz kerak: Debugging metodologiyasi, Bekor qilish: AbortController, Xotira sizishlari va ularni topish, Birinchi avtomatik test: node:test va assert, JavaScript'ning mashhur tuzoqlari.

1. Nega bu kerak?

«Bahor»da onlayn bron ishga tushdi. Kechqurun ikki mehmon — Dilshod aka va Bobur — oxirgi porsiya oshni deyarli bir vaqtda bron qildi. Ikkalasiga ham "Tasdiqlandi" xabari keldi. Oshxonada esa bitta porsiya.

Sardor kodni ochdi. Mantiq to'g'ri: "qoldiq bormi? — bor bo'lsa, birini ayir". O'z kompyuterida yuz marta sinadi — hammasi joyida. Xato faqat ikki so'rov bir vaqtda kelganda chiqadi. Sardorning kompyuterida esa ular hech qachon bir vaqtda kelmaydi.

Bunday xatolar eng qimmat xatolar. Oddiy xato har safar chiqadi — siz uni ko'rasiz va tuzatasiz. Nozik xato esa sinovdan o'tadi, ishga chiqadi va bir haftadan keyin "bitta mijozda, bir marta" chiqadi. Dasturchi "menda ishlayapti" deydi va u haq — uning kompyuterida haqiqatan ishlayapti.

Debugging metodologiyasi darsida xato qidirishning umumiy tartibini o'rgangan edik. Bu dars uning eng qiyin bobi: xato har safar takrorlanmasa nima qilish kerak.

flowchart TD
  A["Xato ba'zan chiqadi"] --> B["Nimaga bog'liq? tartib, vaqt, sana, tasodif, xotira"]
  B --> C["Shu omilni o'zingiz boshqaring"]
  C --> D{"Xato har safar chiqyaptimi?"}
  D -- "yo'q" --> B
  D -- "ha" --> E["Sababni toping"]
  E --> F["Avval test, keyin tuzatish"]

Diagrammadagi asosiy fikr: nozik xatoni "ushlash" mumkin emas, uni majburlash kerak. Xato qaysi omilga bog'liqligini topasiz va o'sha omilni o'zingiz boshqarasiz.

2. Race condition

2.1 Xatoni qayta yaratamiz

Bekor qilish: AbortController darsida race condition (poyga holati) ni ko'rgan edik: natija ishlar qaysi tartibda tugashiga bog'liq bo'lib qoladi. U yerda eski javob yangisining ustiga yozilgan edi. Bu safar boshqa, yanada xavfli turi — yo'qolgan yangilanish (lost update). «Bahor»ning bron kodi:

js
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
let stock = 1; // oxirgi porsiya osh

async function book(guest) {
  const available = stock; // 1. o'qish
  await wait(10); // 2. to'lovni tekshirish (tarmoq)
  if (available > 0) {
    stock = available - 1; // 3. yozish
    console.log(`${guest}: tasdiqlandi`);
  } else {
    console.log(`${guest}: tugadi`);
  }
}

await Promise.all([book("Dilshod aka"), book("Bobur")]);
console.log("qoldiq:", stock);

Konsolda:

text
Dilshod aka: tasdiqlandi
Bobur: tasdiqlandi
qoldiq: 0

Bitta porsiya — ikkita tasdiq. Xato har safar chiqyapti, chunki biz ikki bronni ataylab bir vaqtda boshladik (Promise.all). Sardorning kompyuterida esa u qo'lda, bittadan sinardi. Xatoni takrorlashning siri shu: "bir vaqtda"ni o'zingiz yaratish.

2.2 Qadamma-qadam: nima bo'ldi?

Quyidagi qadamlarda available o'zgaruvchisini kuzating. Bobur stock ni Dilshod aka uni o'zgartirishidan oldin o'qiydi:

Xato naqshi doim bir xil: o'qish → kutish → eski qiymat asosida yozish. Kutish paytida dunyo o'zgargan bo'lishi mumkin, siz esa await dan oldingi suratga qarab qaror qilyapsiz. JavaScript'ning mashhur tuzoqlari darsida ESLint'ning require-atomic-updates qoidasi aynan shu naqshni "Possible race condition" deb belgilaganini ko'rgan edik.

2.3 Tuzatish: navbat

Bitta yechim — tekshirish va yozishni bo'linmas qilish: bitta bron tugamaguncha, ikkinchisi boshlanmasin. Buning uchun Promise'lardan navbat yasaymiz:

js
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
let stock = 1;

async function book(guest) {
  const available = stock;
  await wait(10);
  if (available > 0) {
    stock = available - 1;
    console.log(`${guest}: tasdiqlandi`);
  } else {
    console.log(`${guest}: tugadi`);
  }
}

// Navbat: har yangi ish oldingisi tugagach boshlanadi
let tail = Promise.resolve();
function enqueue(task) {
  const result = tail.then(task);
  tail = result.catch(() => {}); // xato navbatni to'xtatmasin
  return result;
}

await Promise.all([
  enqueue(() => book("Dilshod aka")),
  enqueue(() => book("Bobur")),
]);
console.log("qoldiq:", stock);

Konsolda:

text
Dilshod aka: tasdiqlandi
Bobur: tugadi
qoldiq: 0

enqueue har yangi ishni oldingi ishning Promise'iga .then bilan ulaydi (Promise zanjiri). catch — bitta bron xato bilan tugasa ham, keyingisi kutib qolmasligi uchun.

Diqqat: Bu navbat faqat bitta dastur ichida ishlaydi. Haqiqiy «Bahor» saytida bronlar serverda, ko'p mehmonning brauzeridan keladi. U yerda yechim — ma'lumotlar bazasining o'zi: "qoldiq 0 dan katta bo'lsa — ayir" amalini bitta bo'linmas buyruq qilish (tranzaksiya). Buni backend qismida o'rganamiz. G'oya esa shu: tekshirish va yozish orasida hech kim aralashmasin.

2.4 Ikkinchi tur: eski javob yangisini bosib ketadi

API klient qatlami darsida race condition'ning boshqa turini ko'rgan edik: foydalanuvchi filtrni ikki marta tez almashtiradi, birinchi so'rovning javobi esa keyin keladi va ekranga eski ro'yxat chiziladi. Davosi — eskisini bekor qilish (AbortController) yoki "faqat oxirgi so'rov hisoblanadi" qoidasi.

vazifalar dagi sinxronla() aynan shunday himoyalangan: yangi sinxronlashdan oldin eski GET abort() qilinadi, natija esa faqat sinxronPulti === pult bo'lsa yoziladi. Bu shart "men hali ham eng oxirgi so'rovmanmi?" degan savol.

Tekshirib ko'ring: book ichida const available = stock; qatorini await wait(10); dan keyinga ko'chirsak, xato yo'qoladimi?

Javob

Shu misolda — ha: ikkalasi ham taymerdan keyin, birin-ketin o'qiydi va Bobur 0 ni ko'radi. Lekin bu omadga bog'liq tuzatish. Agar o'qish va yozish orasida yana bitta await paydo bo'lsa (masalan, to'lovni tasdiqlash), xato qaytadi. Ishonchli yechim — tekshirish va yozishni bo'linmas qilish: navbat, serverda esa tranzaksiya.

3. Xotira sizishi

Xotira sizishlari va ularni topish darsida xotira sizishi (memory leak) ni batafsil o'lchagan edik: kerak bo'lmagan, lekin hali yetib boriladigan qiymat. U ham nozik xato: birinchi daqiqada hech narsa sezilmaydi, sahifa soatlab ochiq tursa — sekinlashadi va qotadi.

Uning eng ko'p uchraydigan shaklini bitta hisoblagich bilan ko'rish mumkin — tinglovchini noto'g'ri joyda qo'shish:

js
const listeners = [];
const subscribe = (handler) => listeners.push(handler);

function render(items) {
  // ❌ har chizishda yana bitta tinglovchi
  subscribe(() => items.length);
}

for (let i = 0; i < 100; i++) {
  render(["Non olish"]);
}
console.log("tinglovchilar:", listeners.length);
// tinglovchilar: 100

Ekran 100 marta chizildi — va endi bitta xabar kelsa, 100 ta ishlovchi ishlaydi. Har biri o'z items ini closure'da ushlab turibdi, shuning uchun hech biri axlatga chiqmaydi. Tuzatish — tinglovchini bir marta, modul darajasida qo'shish. vazifalar dagi tingla(boshqaTabdanYangila) aynan shunday: asosiy.js ning oxirida, bir marta.

Sizish nozik bo'lgani uchun uni "ko'z bilan" emas, o'lchov bilan topasiz: amalni ko'p marta takrorlab, xotira qaytmasligini ko'rish va heap snapshot'larni taqqoslash. Bu usullar — o'sha darsda.

4. Off-by-one: bittaga adashish

Off-by-one (bittaga adashish) — chegara bitta ortiq yoki bitta kam olingan xato: < o'rniga <=, 0 dan sanash o'rniga 1 dan. Nozik tomoni — u faqat chegarada, masalan oxirgi sahifada yoki bo'sh ro'yxatda ko'rinadi.

js
const menu = ["Osh", "Lag'mon", "Manti", "Ko'k choy", "Somsa"];

// Sahifa raqami ekranda 1 dan boshlanadi
function getPage(items, pageNumber, pageSize) {
  const start = pageNumber * pageSize;
  return items.slice(start, start + pageSize);
}

console.log(getPage(menu, 1, 2)); // [ 'Manti', "Ko'k choy" ]

Mehmon "1-sahifa"ni bosdi — osh va lag'mon o'rniga manti va ko'k choy chiqdi. Osh va lag'mon esa hech qachon ko'rinmaydi. Sababi: funksiya sahifani 0 dan sanaydi, ekran — 1 dan. Tuzatish — bitta joyda, aniq: const start = (pageNumber - 1) * pageSize;.

Bunday xatoni ushlashning eng yaxshi yo'li — chegaraviy holatlar uchun test (Chegaraviy holatlar): birinchi sahifa, oxirgi to'liq bo'lmagan sahifa (Somsa yolg'iz), bo'sh menyu, mavjud bo'lmagan sahifa.

Tekshirib ko'ring: Tuzatilgan funksiyada getPage(menu, 3, 2) nima qaytaradi?

Javob

[ 'Somsa' ]. start = (3 - 1) * 2 = 4, slice(4, 6) — 4-indeksdan boshlab ikkita, lekin menyuda faqat bittasi qolgan. slice ro'yxat tugasa xato bermaydi, borini qaytaradi. Aynan shu "oxirgi to'liq bo'lmagan sahifa" — chegaraviy holat, uni testda albatta tekshiring.

5. Kasr son: faqat ba'zi narxlarda

JavaScript'ning mashhur tuzoqlari darsida 0.1 + 0.2 ni ko'rgan edik. Nozik xato sifatida u yanada makkor: ko'p qiymatda to'g'ri ishlaydi va faqat ba'zilarida bir so'mga adashadi. «Bahor» chekiga 15 foiz xizmat haqi qo'shamiz (misol uchun):

js
const withService = (price) => Math.floor(price * 1.15);

for (const price of [5000, 6000, 35000]) {
  console.log(price, "→", withService(price));
}

Konsolda:

text
5000 → 5750
6000 → 6899
35000 → 40250

5 000 va 35 000 — to'g'ri. 6 000 da esa 6 900 o'rniga 6 899. Sababi: 6000 * 1.15 aslida 6899.999999999999 (Number turi JS'da), Math.floor esa kasrni tashlab yuboradi. Sardor 5 000 va 35 000 bilan sinagan bo'lsa, xatoni hech qachon ko'rmaydi.

Tuzatish — kasr koeffitsient o'rniga butun sonlar bilan hisoblash: Math.floor((price * 115) / 100) — 6 000 uchun 6 900. Takrorlash usuli esa — bir-ikki narx emas, ko'p qiymatni sikl bilan tekshirish: masalan, 1 000 dan 100 000 gacha har narxni butun sonli variant bilan solishtirish.

6. Vaqt zonasi va sana xatolari

6.1 "Menda 5-oktabr, mijozda 4-oktabr"

js
// faqat sana — UTC bo'yicha yarim tun
const booking = new Date("2026-10-05");
const day = (timeZone) =>
  new Intl.DateTimeFormat("en-CA", { timeZone }).format(booking);

console.log(day("Asia/Tashkent")); // 2026-10-05
console.log(day("America/New_York")); // 2026-10-04

Faqat sanadan iborat satr UTC bo'yicha yarim tun deb o'qiladi (Date va vaqt zonalari). Toshkentda bu 05:00, sana o'zgarmaydi. Nyu-Yorkda esa oldingi kunning kechqurun soat 20:00 si. Toshkentda ishlaydigan dasturchi xatoni hech qachon ko'rmaydi, chet eldagi mijoz esa bronini bir kun oldin ko'radi. Natija Node 24 da ham, Chrome 154 da ham bir xil.

Takrorlash usuli: zonani o'zingiz bering. Node'da — TZ=America/New_York node fayl.js (Git Bash'da), kodda — timeZone opsiyasi bilan.

6.2 Faqat oyning oxirida yiqiladigan test

Bron muddati — "bugundan bir oy keyin". Kod va test:

js
// muddat.test.js
import { test } from "node:test";
import assert from "node:assert/strict";

function addOneMonth(today = new Date()) {
  const date = new Date(today);
  date.setMonth(date.getMonth() + 1);
  return date;
}

test("bir oy keyin — keyingi oy", () => {
  const today = new Date();
  const next = addOneMonth(today);
  assert.equal(next.getMonth(), (today.getMonth() + 1) % 12);
});

Biz uni 2026-yil 5-oktabrda ishga tushirdik — ✔, o'tdi. Lekin 31-yanvarda ishga tushirsak-chi? 31-fevral yo'q, setMonth sanani 3-martga "toshirib" yuboradi (Date asoslari). Test oyning 29–31-kunlarida ba'zan yiqiladi, qolgan kunlari o'tadi. Bunday testni vaqt bombasi deyishadi.

Takrorlash uchun vaqtni o'zimiz boshqaramiz. node:test da soxta soat bor: t.mock.timers.enable Date ni biz bergan lahzaga "to'xtatib" qo'yadi (Birinchi avtomatik test darsidagi mock ning bir turi):

js
test("31-yanvarda ham keyingi oy", (t) => {
  t.mock.timers.enable({
    apis: ["Date"],
    now: new Date("2026-01-31T12:00:00+05:00"),
  });
  const today = new Date();
  const next = addOneMonth(today);
  assert.equal(next.getMonth(), (today.getMonth() + 1) % 12);
});

Haqiqiy chiqish (Node 24.21):

text
✖ 31-yanvarda ham keyingi oy (2.5402ms)
  AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:

  2 !== 1

Tarjimasi: "qiymatlar qat'iy teng bo'lishi kutilgan edi: 2 ≠ 1". Oy 2 (mart) chiqdi, 1 (fevral) kutilgan edi. Endi xato har safar, istalgan kuni chiqadi — uni tuzatish mumkin. Tuzatishning o'zi — 3-mashqda.

Tekshirib ko'ring: Nega testda new Date() (hozirgi vaqt) ishlatish xavfli?

Javob

Chunki test natijasi u qachon ishga tushirilganiga bog'liq bo'lib qoladi. Bugun o'tadi, oyning oxirida, yil boshida yoki yozgi vaqtga o'tish kunida yiqilishi mumkin. Testdagi vaqt — har doim aniq: new Date("2026-01-31T12:00:00+05:00") yoki soxta soat.

7. Flaky test

7.1 Kod o'zgarmadi — test yiqildi

Flaky test (beqaror test) — kod o'zgarmasa ham goh o'tadigan, goh yiqiladigan test. Uning zarari ikki tomonlama. Birinchidan, u haqiqiy xatoni yashirishi mumkin. Ikkinchidan, jamoa qizil testga ishonmay qo'yadi: "e, u o'zi shunaqa, qayta ishga tushir". Ana shu payt haqiqiy xato o'tib ketadi.

Asosiy sabablari:

Sabab Misol Davo
Testlar umumiy holatni bo'lishadi bitta cart massivi har test o'z ma'lumoti bilan
Hozirgi vaqt 31-yanvar aniq sana yoki soxta soat
Tasodif Math.random() tasodifni parametr qilib berish
Tarmoq haqiqiy API soxta fetch, tarmoq testlari alohida
Taymer va tezlik "100 ms da tugaydi" vaqtga emas, hodisaga tayanish

7.2 Tartibga bog'liq test

js
// savat.test.js
import { test } from "node:test";
import assert from "node:assert/strict";

const cart = []; // ❌ hamma testlar uchun bitta

test("osh qo'shiladi", () => {
  cart.push({ dish: "Osh", price: 35000 });
  assert.equal(cart.length, 1);
});

test("jami narx", () => {
  const total = cart.reduce((sum, t) => sum + t.price, 0);
  assert.equal(total, 35000);
});

node --test savat.test.js — ikkala test ham ✔. Lekin "jami narx" testi faqat birinchi test oldin ishlaganda o'tadi. Uni yolg'iz ishga tushirsak (--test-name-pattern="jami"), yiqiladi: actual: 0, expected: 35000.

Node 24 testlarni tasodifiy tartibda ishlatishi mumkin — --test-randomize. Tartib bitta son — seed (urug') bilan belgilanadi. Bir xil seed — bir xil tartib, ya'ni tasodifni takrorlash mumkin:

bash
node --test --test-randomize --test-random-seed=1 savat.test.js
text
✖ jami narx (1.5702ms)
✔ osh qo'shiladi (0.2169ms)
ℹ Randomized test order seed: 1
ℹ tests 2
ℹ pass 1
ℹ fail 1

Biz 1 dan 8 gacha seed'larni sinadik: 1–6 da test yiqildi, 7 va 8 da o'tdi. Seed'siz --test-randomize har safar yangi seed tanlaydi va uni chiqishda yozadi. Yiqilgan bo'lsa — o'sha seed bilan xatoni istagancha takrorlaysiz.

Tuzatish — testlarni bir-biridan ajratish:

js
// ✅ har test o'z savatini yasaydi
function newCart() {
  return [{ dish: "Osh", price: 35000 }];
}

test("ko'k choy qo'shiladi", () => {
  const cart = newCart();
  cart.push({ dish: "Ko'k choy", price: 5000 });
  assert.equal(cart.length, 2);
});

test("jami narx", () => {
  const cart = newCart();
  const total = cart.reduce((sum, t) => sum + t.price, 0);
  assert.equal(total, 35000);
});

Endi 1–6 seed'larning hammasida ℹ fail 0. vazifalar testlari ham aynan shu qoida bilan yozilgan: har test yangiRoyxat() bilan o'z ro'yxatini oladi.

7.3 Tarmoqqa bog'liq test

vazifalar da bu qaror allaqachon qabul qilingan: haqiqiy «Bahor» API'siga boradigan testlar api.tarmoq.js faylida, npm test dan tashqarida (npm run test:tarmoq). Sababi — internet sekin bo'lsa yoki server qayta ishga tushsa, ular kod aybsiz bo'lsa ham yiqiladi. npm test esa fetch o'rniga soxta funksiya bilan, internetsiz va har safar bir xil ishlaydi.

Diqqat: Flaky testni "3 marta qayta urin, birida o'tsa — yashil" sozlamasi bilan yashirmang. Bu xatoni o'chirmaydi, faqat ko'rinmas qiladi. Qayta urinish — faqat sababi topilgunicha vaqtinchalik chora, va u yozib qo'yiladi (Legacy kod va texnik qarz darsidagi TEXNIK-QARZ.md ga).

8. Heisenbug

8.1 Kuzatsangiz — o'zgaradi

Heisenbug — siz uni o'rganmoqchi bo'lganingizda o'zgaradigan yoki yo'qoladigan xato. Nomi fizikdan — Heisenbergning "noaniqlik prinsipi"dan: zarrachani o'lchash uning holatini o'zgartiradi. Teskarisi — Bohrbug: har safar, bir xil sharoitda bir xil chiqadigan "yaxshi" xato.

Mana JavaScript'dagi haqiqiy Heisenbug. Izohlarda telefon bor-yo'qligini tekshiramiz:

js
const PHONE = /\+998 \d{2}/g;
const notes = ["+998 90 000 00 00", "+998 91 000 00 00"];

for (const note of notes) {
  if (PHONE.test(note)) console.log("qabul:", note);
  else console.log("rad:", note);
}

Konsolda:

text
qabul: +998 90 000 00 00
rad: +998 91 000 00 00

Ikkinchi izohda ham telefon bor — nega rad? Sardor tekshirish uchun bitta qator qo'shdi:

js
const PHONE = /\+998 \d{2}/g;
const notes = ["+998 90 000 00 00", "+998 91 000 00 00"];

for (const note of notes) {
  if (PHONE.test(note)) console.log("qabul:", note);
  else console.log("rad:", note);
  console.log("  tekshiruv:", PHONE.test(note)); // debug
}

Konsolda:

text
qabul: +998 90 000 00 00
  tekshiruv: false
qabul: +998 91 000 00 00
  tekshiruv: false

Xato yo'qoldi — ikkalasi ham qabul. Ustiga-ustak, debug qatori ikki marta false deyapti, garchi telefon aniq bor. Sardor debug qatorini o'chirsa — xato qaytadi.

8.2 Nega?

Sabab — Flaglar va lastIndex tuzog'i dagi g flag. test topgan joyini lastIndex ga yozadi va keyingi safar o'sha yerdan qidiradi. Topolmasa — lastIndex ni 0 ga qaytaradi.

  • Debug'siz: 1-izohda test topdi (lastIndex = 7). 2-izohda 7-belgidan qidirdi — topolmadi, rad.
  • Debug bilan: 1-izohdan keyin debug test 7-belgidan qidirdi, topolmadi va lastIndex ni 0 ga qaytardi. Shuning uchun 2-izoh boshidan tekshirildi va topildi.

Debug qatori o'zi holatni o'zgartirdi. Heisenbug'ning umumiy sababi ham shu: kuzatish usuli dastur holatiga yoki vaqtiga ta'sir qiladi. Boshqa misollar:

  • Holatni o'zgartiradigan getter. DevTools obyektni ochganda getter'ni chaqiradi — getter ichida hisoblagich bo'lsa, u o'sadi (Getter va setter).
  • Vaqt. Breakpoint yoki ko'p console.log kodni sekinlashtiradi. Race condition'dagi ikki ish endi bir vaqtda kelmaydi va xato yo'qoladi.
  • Brauzer konsoli. Chrome'da console.log(obj) qilib, keyinroq obyektni ochsangiz, siz log paytidagi emas, ochgan paytdagi holatni ko'rasiz. Kichik "i" belgisi shu haqda ogohlantiradi. Aniq surat kerak bo'lsa — console.log(structuredClone(obj)) yoki JSON.stringify.

Davosi: kuzatish usulini "jim" qiling — holatni o'zgartirmaydigan, vaqtga ta'sir qilmaydigan. PHONE.test(note) o'rniga debug'da note.match(/\+998 \d{2}/) (g'siz, yangi regex). Xatoning o'zi esa — test uchun g flagni olib tashlash.

Tekshirib ko'ring: Race condition bor kodda xato faqat console.log lar olib tashlanganda chiqadi. Bu qanday turdagi xato va nega?

Javob

Heisenbug. console.log lar kodni ozgina sekinlashtirib, ishlarning tugash tartibini o'zgartiradi — "poyga" boshqa natija bilan tugaydi. Log'lar olib tashlangach, tartib qaytadi va xato ham qaytadi. Davo: tartibni log bilan emas, ataylab boshqarish (navbat, bekor qilish) va xatoni Promise.all bilan majburlab test qilish.

9. Nozik xatoni qidirish tartibi

Barcha turlar uchun bitta yo'l:

  1. Omilni toping. Xato qachon chiqadi: tez bosilganda? Kechasi? Boshqa zonada? Testlar boshqa tartibda? Uzoq ochiq turganda?
  2. Omilni boshqaring. Tartib — Promise.all va --test-randomize --test-random-seed. Vaqt — soxta soat yoki aniq Date. Zona — timeZone/TZ. Tarmoq — DevTools'da "Slow 4G" yoki soxta fetch. Takrorlash — amalni sikl ichida 1000 marta.
  3. Xato har safar chiqsin. Shundan keyingina sabab qidiriladi. "Ba'zan" chiqadigan xatoni tuzatdim deb ishonib bo'lmaydi.
  4. Avval test, keyin tuzatish. Xatoni ushlagan testni yozing — u qizil bo'lsin. Tuzating — yashil bo'lsin. Test kodda qoladi va xato qaytib kelsa, darhol aytadi.
  5. Izoh qoldiring. Kod nega aynan shunday (navbat, setDate(1)) — "nega" izohi (Toza kod: funksiyalar, izohlar va tuzilma). Aks holda kimdir "ortiqcha" deb olib tashlaydi.

Qachondan buzilganini topish kerak bo'lsa — git bisect (yarim bo'lish usuli bilan commit'larni tekshirish) yordam beradi; u haqda Debugging metodologiyasi darsida eslatgan edik.

10. Ko'p uchraydigan xatolar

10.1 setTimeout bilan "tuzatish"

Ikki ish to'qnashdi — biriga setTimeout(…, 100) qo'yib, "endi ishlayapti". Sekin telefonda 100 ms yetmaydi va xato qaytadi.

Tartibni aniq qiling: await, navbat, AbortController.

10.2 Testda sleep

await wait(500) — "server javob berib ulgursin". Test sekin va baribir ba'zan yiqiladi.

Aniq hodisani kuting (Promise'ning o'zi) yoki vaqtni soxta soat bilan boshqaring.

10.3 Yiqilgan testni qayta ishga tushirish

CI'da qizil — "Re-run" tugmasi — yashil — davom.

Seed'ni, sanani va muhitni yozib oling. Bir marta yiqilgan test — xato bor degani.

10.4 Debug kodini tuzatish deb qoldirish

console.log qo'shilgach xato yo'qoldi — log qoldi.

Bu Heisenbug belgisi: log nima o'zgartirganini toping. Sabab o'sha yerda.

11. Mashqlar

1-mashq (oson): Turini ayting

Har vaziyat qaysi turdagi nozik xato: race condition, xotira sizishi, off-by-one, vaqt zonasi, flaky test yoki Heisenbug?

  1. Sahifa 3 soat ochiq tursa, sekinlashadi va oxiri qotadi.
  2. Test CI'da ba'zan yiqiladi, lokalda doim o'tadi; testlar tartibi har safar boshqacha.
  3. Breakpoint qo'yilsa, xato chiqmaydi.
  4. Ro'yxatning oxirgi elementi hech qachon ko'rsatilmaydi.
Yechim
  1. Xotira sizishi — har amalda qoladigan narsa vaqt o'tib to'planadi.
  2. Flaky test — sababi, ehtimol, testlar umumiy holatni bo'lishishi; --test-randomize bilan tekshiring.
  3. Heisenbug — breakpoint vaqtni o'zgartiradi, ehtimol ichida race condition bor.
  4. Off-by-one — sikl chegarasi (< va <=) yoki slice ning oxiri.

2-mashq (o'rta): Ombordagi race condition

Quyidagi kod ikki ofitsiantning bir vaqtda ko'k choy olishini modellashtiradi. Omborda 1 ta choynak, lekin ikkalasi ham "oldi" deyapti. Navbat bilan tuzating — ikkinchisi "qolmadi" desin. Ishora: «Tuzatish: navbat» bo'limidagi enqueue.

js
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
let teapots = 1;

async function take(waiter) {
  const available = teapots;
  await wait(5);
  if (available > 0) {
    teapots = available - 1;
    return `${waiter}: oldi`;
  }
  return `${waiter}: qolmadi`;
}

const result = await Promise.all([
  take("Otabek"),
  take("Rustam aka"),
]);
console.log(result); // [ 'Otabek: oldi', 'Rustam aka: oldi' ]
Yechim
js
const wait = (ms) => new Promise((done) => setTimeout(done, ms));
let teapots = 1;

async function take(waiter) {
  const available = teapots;
  await wait(5);
  if (available > 0) {
    teapots = available - 1;
    return `${waiter}: oldi`;
  }
  return `${waiter}: qolmadi`;
}

let tail = Promise.resolve();
function enqueue(task) {
  const result = tail.then(task);
  tail = result.catch(() => {});
  return result;
}

const result = await Promise.all([
  enqueue(() => take("Otabek")),
  enqueue(() => take("Rustam aka")),
]);
console.log(result); // [ 'Otabek: oldi', 'Rustam aka: qolmadi' ]

Promise.all natijalarni chaqiruv tartibida qaytaradi, shuning uchun chiqish har safar bir xil. Navbat Otabekning ishi tugagach Rustam akanikini boshlaydi — Rustam aka teapots ni allaqachon 0 holatida o'qiydi.

3-mashq (qiyin): Vaqt bombasini zararsizlantiring

«Faqat oyning oxirida yiqiladigan test» bo'limidagi addOneMonth ni shunday tuzating: 31-yanvar + 1 oy = 28-fevral (oyning oxirgi kuni), 5-oktabr + 1 oy = 5-noyabr. Ikkala holat uchun aniq sanali test yozing va kurs/mashqlar/12/42-nozik/muddat.test.js da ishga tushiring. Ishora: avval setDate(1), keyin oyni oshiring; oyning oxirgi kuni — new Date(year, month + 1, 0).getDate() ("keyingi oyning 0-kuni").

Yechim
js
// muddat.test.js
import { test } from "node:test";
import assert from "node:assert/strict";

function addOneMonth(today) {
  const date = new Date(today);
  const day = date.getDate();
  date.setDate(1); // avval 1-kunga — oy "toshmaydi"
  date.setMonth(date.getMonth() + 1);
  const lastDay = new Date(
    date.getFullYear(), date.getMonth() + 1, 0,
  ).getDate();
  date.setDate(Math.min(day, lastDay));
  return date;
}

test("31-yanvar + 1 oy = 28-fevral", () => {
  const next = addOneMonth(new Date("2026-01-31T12:00:00+05:00"));
  assert.equal(next.getMonth(), 1);
  assert.equal(next.getDate(), 28);
});

test("5-oktabr + 1 oy = 5-noyabr", () => {
  const next = addOneMonth(new Date("2026-10-05T12:00:00+05:00"));
  assert.equal(next.getMonth(), 10);
  assert.equal(next.getDate(), 5);
});

node --test muddat.test.js — ℹ pass 2, ℹ fail 0. Biz uni ikki zonada sinadik: TZ=Asia/Tashkent va TZ=America/New_York — ikkalasida ham o'tdi (soat 12:00 tanlangani uchun sana zonada siljimaydi). Parametr endi majburiy: today ni chaqiruvchi beradi, test esa aniq sanani. Temporal'da xuddi shu ish bitta qator: PlainDate.from("2026-01-31").add({ months: 1 }) — 28-fevral (Temporal API).

bash
git add 12/42-nozik/muddat.test.js
git commit -m "12/42: birOyKeyin vaqt bombasi tuzatildi"

4-mashq: Vazifalar qadami — testlar tartibga bog'liqmi?

vazifalar v3.1 papkasida testlarni uch xil tasodifiy tartibda ishga tushiring:

bash
node --test --test-randomize --test-random-seed=1
node --test --test-randomize --test-random-seed=2
node --test --test-randomize --test-random-seed=3

Hammasi o'tadimi? Natijani va testlar nega tartibga bog'liq emasligini kurs/mashqlar/12/42-nozik/vazifalar-tartib.md ga yozing. vazifalar kodini o'zgartirmang.

Yechim

Bizdagi haqiqiy natija (Node 24.21):

text
ℹ Randomized test order seed: 1   ℹ tests 59  ℹ pass 59  ℹ fail 0
ℹ Randomized test order seed: 2   ℹ tests 59  ℹ pass 59  ℹ fail 0
ℹ Randomized test order seed: 3   ℹ tests 59  ℹ pass 59  ℹ fail 0

Sabablari: har test yangiRoyxat() bilan o'z ro'yxatini oladi (#35 dagi qaror: eski skript bitta umumiy ro'yxatni o'zgartirardi). Bundan tashqari: localStorage o'rnida xotira.js dagi o'rinbosar; fetch — soxta funksiya (tikuv orqali); haqiqiy tarmoq testlari npm test dan tashqarida. Shunday qilib, testlarda umumiy holat, hozirgi vaqt va tarmoq — uchala flaky manbai yo'q.

bash
git add 12/42-nozik/vazifalar-tartib.md
git commit -m "12/42: vazifalar testlari tasodifiy tartibda"

12. Real ishda

  • Katta jamoalar flaky testlarni kuzatadi: qaysi test qancha marta "goh qizil, goh yashil" bo'lganini hisoblaydi va bunday testni alohida ro'yxatga (karantinga) olib, egasini tayinlaydi. Maqsad — CI'dagi qizil rangga ishonch saqlansin.
  • Race condition — backend'ning kundalik ishi: pul o'tkazish, oxirgi chipta, ombor qoldig'i. Ularni ma'lumotlar bazasidagi tranzaksiya va bloklash hal qiladi — backend qismida.
  • Vaqt bilan ishlaydigan kod (obuna muddati, chegirma tugashi, cron — jadval bo'yicha o'zi ishga tushadigan vazifa) har doim "soat"ni parametr qilib oladi — shunda uni testda boshqarish mumkin.
  • Intervyu: "Race condition nima? Misol keltiring", "Flaky testni qanday tuzatgansiz?", "Faqat production'da chiqadigan xatoni qanday topasiz?" — middle darajadagi intervyularda ko'p so'raladi. Javobda "takrorlash — omilni boshqarish — test" tartibini aytish katta ustunlik.

Xulosa

  • Nozik xato — ba'zan chiqadigan xato; uni ushlash emas, omilni boshqarib majburlash kerak.
  • Race condition: o'qish → await → eski qiymat asosida yozish. Davo — tekshirish va yozishni bo'linmas qilish (navbat, serverda tranzaksiya) yoki eski so'rovni bekor qilish.
  • Xotira sizishi va off-by-one — chegarada va vaqt o'tib ko'rinadi; o'lchov va chegaraviy testlar bilan topiladi.
  • Kasr son — faqat ba'zi qiymatlarda bir so'mga adashadi; pulni butun sonlarda hisoblang va ko'p qiymat bilan sinang.
  • Sana: faqat sanali satr — UTC; setMonth oyni "toshiradi"; testda vaqt — aniq yoki soxta soat.
  • Flaky test: umumiy holat, vaqt, tasodif, tarmoq; --test-randomize --test-random-seed tartibni takrorlaydi.
  • Heisenbug: kuzatish holat yoki vaqtni o'zgartiradi (g regex'li debug, getter, breakpoint); kuzatishni "jim" qiling.

Keyingi dars: AI bilan samarali ishlash: prompting va kontekst — AI yordamchiga aniq vazifa va kerakli kontekstni berish, undan test yozdirish va javobini tekshirish.

Manbalar

  • Node.js hujjatlari: "Test runner" — --test-randomize, --test-random-seed, --test-name-pattern, mock.timers — nodejs.org/api/test.html
  • ESLint: require-atomic-updates — eslint.org/docs/latest/rules/require-atomic-updates
  • MDN: RegExp.prototype.lastIndex, Date.prototype.setMonth(), Intl.DateTimeFormat (timeZone) — developer.mozilla.org
  • Chrome DevTools: "Console features reference" (obyekt qiymati ochilgan paytda baholanadi) — developer.chrome.com/docs/devtools/console
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Nozik xatolar: race condition, xotira sizishi, flaky test va Heisenbug — IlmHamroh