Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. Race condition
- 2.1 Xatoni qayta yaratamiz
- 2.2 Qadamma-qadam: nima bo'ldi?
- 2.3 Tuzatish: navbat
- 2.4 Ikkinchi tur: eski javob yangisini bosib ketadi
- 3. Xotira sizishi
- 4. Off-by-one: bittaga adashish
- 5. Kasr son: faqat ba'zi narxlarda
- 6. Vaqt zonasi va sana xatolari
- 6.1 "Menda 5-oktabr, mijozda 4-oktabr"
- 6.2 Faqat oyning oxirida yiqiladigan test
- 7. Flaky test
- 7.1 Kod o'zgarmadi — test yiqildi
- 7.2 Tartibga bog'liq test
- 7.3 Tarmoqqa bog'liq test
- 8. Heisenbug
- 8.1 Kuzatsangiz — o'zgaradi
- 8.2 Nega?
- 9. Nozik xatoni qidirish tartibi
- 10. Ko'p uchraydigan xatolar
- 10.1 setTimeout bilan "tuzatish"
- 10.2 Testda sleep
- 10.3 Yiqilgan testni qayta ishga tushirish
- 10.4 Debug kodini tuzatish deb qoldirish
- 11. Mashqlar
- 1-mashq (oson): Turini ayting
- 2-mashq (o'rta): Ombordagi race condition
- 3-mashq (qiyin): Vaqt bombasini zararsizlantiring
- 4-mashq: Vazifalar qadami — testlar tartibga bog'liqmi?
- 12. Real ishda
- Xulosa
- Manbalar
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-randomizeva soxta soat bilan har safar yiqiladigan qilasiz, keyin tuzatasiz. - Heisenbug nima ekanini va nega
console.logqo'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:
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:
Dilshod aka: tasdiqlandi
Bobur: tasdiqlandi
qoldiq: 0Bitta 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:
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:
Dilshod aka: tasdiqlandi
Bobur: tugadi
qoldiq: 0enqueue 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:
bookichidaconst available = stock;qatoriniawait 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:
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: 100Ekran 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.
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):
const withService = (price) => Math.floor(price * 1.15);
for (const price of [5000, 6000, 35000]) {
console.log(price, "→", withService(price));
}Konsolda:
5000 → 5750
6000 → 6899
35000 → 402505 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"
// 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-04Faqat 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:
// 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):
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):
✖ 31-yanvarda ham keyingi oy (2.5402ms)
AssertionError [ERR_ASSERTION]: Expected values to be strictly equal:
2 !== 1Tarjimasi: "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
// 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:
node --test --test-randomize --test-random-seed=1 savat.test.js✖ jami narx (1.5702ms)
✔ osh qo'shiladi (0.2169ms)
ℹ Randomized test order seed: 1
ℹ tests 2
ℹ pass 1
ℹ fail 1Biz 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:
// ✅ 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.mdga).
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:
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:
qabul: +998 90 000 00 00
rad: +998 91 000 00 00Ikkinchi izohda ham telefon bor — nega rad? Sardor tekshirish uchun bitta qator qo'shdi:
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:
qabul: +998 90 000 00 00
tekshiruv: false
qabul: +998 91 000 00 00
tekshiruv: falseXato 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
testtopdi (lastIndex= 7). 2-izohda 7-belgidan qidirdi — topolmadi, rad. - Debug bilan: 1-izohdan keyin debug
test7-belgidan qidirdi, topolmadi valastIndexni 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.logkodni 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))yokiJSON.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.loglar 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:
- Omilni toping. Xato qachon chiqadi: tez bosilganda? Kechasi? Boshqa zonada? Testlar boshqa tartibda? Uzoq ochiq turganda?
- Omilni boshqaring. Tartib —
Promise.allva--test-randomize --test-random-seed. Vaqt — soxta soat yoki aniqDate. Zona —timeZone/TZ. Tarmoq — DevTools'da "Slow 4G" yoki soxtafetch. Takrorlash — amalni sikl ichida 1000 marta. - Xato har safar chiqsin. Shundan keyingina sabab qidiriladi. "Ba'zan" chiqadigan xatoni tuzatdim deb ishonib bo'lmaydi.
- 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.
- 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?
- Sahifa 3 soat ochiq tursa, sekinlashadi va oxiri qotadi.
- Test CI'da ba'zan yiqiladi, lokalda doim o'tadi; testlar tartibi har safar boshqacha.
- Breakpoint qo'yilsa, xato chiqmaydi.
- Ro'yxatning oxirgi elementi hech qachon ko'rsatilmaydi.
Yechim
- Xotira sizishi — har amalda qoladigan narsa vaqt o'tib to'planadi.
- Flaky test — sababi, ehtimol, testlar umumiy holatni bo'lishishi;
--test-randomizebilan tekshiring. - Heisenbug — breakpoint vaqtni o'zgartiradi, ehtimol ichida race condition bor.
- Off-by-one — sikl chegarasi (
<va<=) yokislicening 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.
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
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
// 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).
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:
node --test --test-randomize --test-random-seed=1
node --test --test-randomize --test-random-seed=2
node --test --test-randomize --test-random-seed=3Hammasi 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):
ℹ 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 0Sabablari: 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.
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;
setMonthoyni "toshiradi"; testda vaqt — aniq yoki soxta soat. - Flaky test: umumiy holat, vaqt, tasodif, tarmoq;
--test-randomize --test-random-seedtartibni takrorlaydi. - Heisenbug: kuzatish holat yoki vaqtni o'zgartiradi (
gregex'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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!