IlmHamroh
JavaScript Full-stack/25-qism. HTTP, Express va REST API32/51-dars21 daqiqa
Mundarija (30)

Express xato turlari va custom error iyerarxiyasi: operatsion xato va dasturchi xatosi

Qisqacha: Serverdagi xatolar ikki xil. Kutilgan (operatsion) xato — "vazifa topilmadi", "matn bo'sh": mijozga aniq status va sabab aytiladi, jarayon sog'. Dasturchi xatosi yoki kutilmagan holat — TypeError, uzilgan baza: mijozga faqat 500 va "kutilmagan xato", tafsilot logga. Ikkalasini ajratish uchun bitta ota klass yasaladi — IlovaXatosi (status, kod, sarlavha, operatsion = true). Undan TopilmadiXatosi (404), ValidatsiyaXatosi (422) kabi bolalar meros oladi. Iyerarxiyada yo'q har xato — avtomatik 500.

Bu darsda

  • Operatsion xato va dasturchi xatosini server misolida ajrata olasiz.
  • IlovaXatosi va uning 6 ta bolasini yozasiz: holat, kod, sarlavha, xatolar, httpSarlavhalar, operatsion, cause.
  • new.target.name nima uchun name ni har bolada qayta yozishdan qutqarishini tushuntira olasiz.
  • Xizmat qatlami xatoni qanday tashlashini va marshrut uni qanday ko'rishini bilasiz.
  • vazifalar-api ning holat 34 ga 4-qadamni qo'yasiz (bitta HttpXato o'rniga iyerarxiya).

Oldin bilishingiz kerak: O'z xato klasslaringiz, Xato strategiyasi, Xato javoblari: Problem Details, Stack trace, source map va error.cause.

1. Nega bu kerak?

Kechasi soat 2 da telefoningizga xabar keldi: "vazifalar-api: 500 xatolar soni oshdi". Logni ochasiz va 300 ta qatorni ko'rasiz. Ulardan 290 tasi — foydalanuvchi bo'sh matn yuborgan, server 422 qaytargan. 8 tasi — mavjud bo'lmagan vazifa so'ralgan. Faqat 2 tasi haqiqiy muammo: kodda undefined.matn o'qilgan.

Agar hamma xato bir xil ko'rinsa, siz o'sha 2 tasini 298 ta "shovqin" ichidan qidirasiz. Ertalab esa yana xabar keladi.

Xato strategiyasi darsida bu ikki turni brauzer misolida ajratgansiz: operatsion xato (operational error) — dastur to'g'ri, lekin tashqi dunyo kutganimizdek emas; dasturchi xatosi (bug) — kodning o'zi noto'g'ri. Serverda bu farq yanada muhim, chunki u uch narsani hal qiladi:

Savol Operatsion Dasturchi xatosi
Mijozga nima? Aniq status va sabab (404, 422) 500 va umumiy matn
Log darajasi warn yoki umuman yo'q error, stack bilan
Kimni uyg'otamiz? Hech kimni Navbatchi dasturchini

O'xshatish: aeroport. "Pasportingiz muddati o'tgan" — operatsion holat: xodim aniq aytadi, navbat davom etadi. "Lenta to'xtab qoldi va yuklar yonib ketdi" — tizim buzilgan: yo'lovchiga "texnik nosozlik" deyiladi, muhandis chaqiriladi.

2. Server xatolari: qaysi biri qaysi tur?

2.1 vazifalar-api dagi haqiqiy holatlar

Keling, API'da bo'lishi mumkin bo'lgan xatolarni ro'yxatlaymiz:

Holat Tur Javob
GET /v1/vazifalar/77 — bunday vazifa yo'q operatsion 404
POST da matn bo'sh operatsion 422
?limit=0 operatsion 400
Vazifalar soni 1000 ga yetdi operatsion 409
If-Match: "3", lekin vazifa 4-versiyada operatsion 412
DELETE /v1/teglar operatsion 405
ombor.ol() ichida undefined.versiya dasturchi xatosi 500
Baza bilan ulanish uzildi kutilmagan 500

Oxirgi qator qiziq. Ulanish uzilishi — kod xatosi emas, tarmoq muammosi. Joyent'ning mashhur maqolasi uni operatsion deb ataydi. Lekin bizning API uchun u kutilmagan: biz uni oldindan bilib, mijozga foydali javob bera olmaymiz. Shuning uchun amaliy qoida:

Maslahat: "Operatsion" — bu "mijozga aniq nima deyishni bilamiz" degani. Bila olsak — o'z xato klassimiz. Bila olmasak — 500.

2.2 Bir xil Error — har xil ma'no

Problem Details darsida holat 24 da bitta klass bor edi — HttpXato(holat, xabar, sozlama). U ishladi, lekin kodda shunday ko'rinardi:

ts
throw new HttpXato(404, `Vazifa ${id} topilmadi`, {
  kod: "vazifa-topilmadi",
  sarlavha: "Vazifa topilmadi",
});

Uch muammo bor. Birinchi: har throw da 404, kod va sarlavha qo'lda takrorlanadi — bir joyda "Vazifa topilmadi", boshqasida "Vazifa yo'q" bo'lib qoladi. Ikkinchi: test "bu 404 mi?" deb faqat raqamni tekshira oladi. Uchinchi: catch da "bu kutilgan xatomi?" degan savolga javob yo'q — HttpXato ham, TypeError ham Error.

Yechim — O'z xato klasslaringiz darsidagi iyerarxiya, endi server uchun.

Tekshirib ko'ring: Mijoz POST /v1/vazifalar ga {"matn": 5} yubordi. Bu qaysi tur va qaysi status?

Javob

Operatsion: dastur to'g'ri ishlayapti, mijoz noto'g'ri ma'lumot yubordi. Status — 422 (JSON sintaksisi to'g'ri, lekin mazmun qoidaga mos emas). Mijozga aniq sabab beriladi: body.matn — satr bo'lishi kerak. Log'ga error yozilmaydi.

3. Ota klass: IlovaXatosi

3.1 Brauzerda sinab ko'ramiz

Kanon TypeScript'da, lekin g'oya oddiy JavaScript'da ham bir xil. Avval soddalashtirilgan nusxa — uni «Ishga tushir» bilan sinang:

js
class IlovaXatosi extends Error {
  operatsion = true;
  constructor(holat, xabar, sozlama = {}) {
    super(xabar, { cause: sozlama.cause });
    this.name = new.target.name;
    this.holat = holat;
    this.kod = sozlama.kod;
  }
}

class TopilmadiXatosi extends IlovaXatosi {
  constructor(xabar, kod) {
    super(404, xabar, { kod });
  }
}

const x = new TopilmadiXatosi("Vazifa 77 yo'q", "vazifa-topilmadi");
console.log(x.name); // TopilmadiXatosi
console.log(x.holat, x.kod); // 404 vazifa-topilmadi
console.log(x instanceof TopilmadiXatosi); // true
console.log(x instanceof IlovaXatosi); // true
console.log(x instanceof Error); // true
console.log(x.operatsion); // true

Konsolda:

text
TopilmadiXatosi
404 vazifa-topilmadi
true
true
true
true

Qatorma-qator:

  • operatsion = true — klass maydoni (class field). Har nusxada bor va iyerarxiyadagi hamma xato uchun bir xil.
  • super(xabar, { cause }) — error.cause darsidagi ikkinchi argument: asl sabab saqlanadi.
  • this.name = new.target.name — eng muhim qator, keyingi bo'limda.
  • instanceof zanjiri: bola — ham o'zi, ham otasi, ham Error.

3.2 new.target.name — nom avtomatik

new.target — konstruktor ichida "aynan qaysi klass bilan new qilindi?" degan savolning javobi. new TopilmadiXatosi(...) chaqirilganda ota konstruktor ichida ham new.target — TopilmadiXatosi.

js
class Ota {
  constructor() {
    console.log("new.target:", new.target.name);
    console.log("this.constructor:", this.constructor.name);
  }
}
class Bola extends Ota {}
new Bola();

Konsolda:

text
new.target: Bola
this.constructor: Bola

Ikkalasi ham ishlaydi. Kanon new.target ni tanladi — u "konstruktor ichida" ekanini aniq bildiradi. Muhimi: name ni bir marta, otada yozasiz. O'n bola qo'shsangiz ham, har birida this.name = "..." qatori kerak emas. O'z xato klasslaringiz darsida name ni qo'yishning uch yo'lini ko'rgansiz — bu ulardan eng qisqasi.

Nega name muhim? Chunki stack trace va log birinchi qatorda aynan uni yozadi: TopilmadiXatosi: Vazifa 77 topilmadi. Error: Vazifa 77 topilmadi dan ko'ra ancha ko'p narsa aytadi.

Diqqat: Kod siqiladigan (minify) muhitda klass nomlari a, b ga aylanishi mumkin. Server kodi odatda siqilmaydi, shuning uchun new.target.name xavfsiz. Frontend'da esa nomni satr bilan yozish ishonchliroq.

3.3 Kanon: src/umumiy/xatolar.ts

Haqiqiy fayl ko'proq maydonga ega. Qisqartirilgan ko'rinishi (kanon, holat 34; uzun qatorlar telefon uchun bo'lindi):

ts
export type MaydonXatosi = { joy: string; xabar: string };

export type XatoSozlamasi = ErrorOptions & {
  kod?: string;      // mashina o'qiydigan: "vazifa-topilmadi"
  sarlavha?: string; // odam o'qiydigan: "Vazifa topilmadi"
  xatolar?: MaydonXatosi[];
  httpSarlavhalar?: Record<string, string>; // Allow, ETag …
};

export class IlovaXatosi extends Error {
  readonly holat: number;
  readonly kod: string | undefined;
  readonly sarlavha: string | undefined;
  readonly xatolar: MaydonXatosi[] | undefined;
  readonly httpSarlavhalar: Record<string, string>;
  /** kutilgan xato — jarayon holati buzilmagan */
  readonly operatsion = true;

  constructor(holat: number, xabar: string,
              sozlama: XatoSozlamasi = {}) {
    super(xabar, sozlama.cause === undefined
      ? undefined : { cause: sozlama.cause });
    this.name = new.target.name;
    this.holat = holat;
    this.kod = sozlama.kod;
    this.sarlavha = sozlama.sarlavha;
    this.xatolar = sozlama.xatolar;
    this.httpSarlavhalar = sozlama.httpSarlavhalar ?? {};
  }
}

Har maydon Problem Details javobining bir qismiga aylanadi:

Maydon Javobda Misol
holat status + HTTP status 404
message detail "Vazifa 77 topilmadi"
kod type URI oxiri va kod vazifa-topilmadi
sarlavha title "Vazifa topilmadi"
xatolar xatolar ro'yxati [{ joy: "body.matn", … }]
httpSarlavhalar HTTP sarlavhalar Allow: GET, HEAD

Ikki TypeScript nozikligi:

  • readonly — xato yaratilgandan keyin o'zgarmaydi. Xato — "nima bo'lgani"ning yozuvi, uni yo'lda tahrirlash chalkashlik keltiradi.
  • cause === undefined ? undefined : { cause } — kanon tsconfig'da exactOptionalPropertyTypes yoqilgan. { cause: undefined } va "cause umuman yo'q" — ikki xil narsa. Stack trace darsida ko'rgansiz: cause berilmasa, xususiyat umuman bo'lmaydi.

4. Bolalar: oltita aniq xato

4.1 Iyerarxiya

flowchart TD
  E[Error] --> I[IlovaXatosi<br/>operatsion = true]
  I --> P[ParametrXatosi 400]
  I --> V[ValidatsiyaXatosi 422]
  I --> T[TopilmadiXatosi 404]
  I --> K[KonfliktXatosi 409]
  I --> S[ShartXatosi 412]
  I --> M[MetodYoqXatosi 405]
  E --> B[TypeError, DB xatosi …<br/>iyerarxiyadan tashqarida]
  B -.-> X[500]

Diagrammaga qarang: chap tomondagi oltita bola — har biri bitta aniq vaziyat. O'ngdagi tarmoq — "biz yaratmagan" hamma xato. Ular qanday tur bo'lishidan qat'i nazar 500 ga boradi.

4.2 Har bola — o'z vaziyati

Kanon bolalari (qisqartirilgan):

ts
/** 400: query yoki yo'l parametri noto'g'ri */
export class ParametrXatosi extends IlovaXatosi {
  constructor(xabar: string, xatolar?: MaydonXatosi[]) {
    super(400, xabar, { kod: "parametr",
      sarlavha: "Parametr noto'g'ri", xatolar });
  }
}

/** 422: tana JSON, lekin qoidaga mos emas */
export class ValidatsiyaXatosi extends IlovaXatosi {
  constructor(xatolar: MaydonXatosi[], xabar = xatolar.length === 1
      ? (xatolar[0]?.xabar ?? "")
      : `${String(xatolar.length)} ta maydon noto'g'ri`) {
    super(422, xabar, { kod: "validatsiya",
      sarlavha: "So'rov ma'lumoti noto'g'ri", xatolar });
  }
}

/** 412: If-Match versiyasi eskirgan — javobda joriy ETag */
export class ShartXatosi extends IlovaXatosi {
  constructor(xabar: string, joriyEtag?: string) {
    super(412, xabar, { kod: "versiya-mos-emas",
      sarlavha: "Vazifa o'zgarib ketgan",
      httpSarlavhalar: joriyEtag ? { ETag: joriyEtag } : {} });
  }
}

Kanonda exactOptionalPropertyTypes tufayli xatolar === undefined ? … : … kabi tarmoqlar bor — bu yerda g'oya ko'rinishi uchun soddalashtirdik. Qolgan uchtasi:

Klass Status Qo'shimcha
TopilmadiXatosi(xabar, kod?, sarlavha?) 404 kodsiz bo'lsa — about:blank (marshrut topilmadi)
KonfliktXatosi(xabar, { kod, … }) 409 kod majburiy: royxat-toldi, idempotentlik-jarayonda
MetodYoqXatosi(metod, allow) 405 Allow sarlavhasi

E'tibor bering: ValidatsiyaXatosi xabarni o'zi yasaydi. Bitta maydon xato bo'lsa — o'sha maydon xabari, bir nechta bo'lsa — "3 ta maydon noto'g'ri". Bu qoida endi bitta joyda, 20 ta throw da emas.

4.3 Qancha bola kerak?

Har status uchun klass yasash shart emas. Qoida — catch qiladigan yoki alohida ishlov beriladigan vaziyat uchun klass. ShartXatosi alohida, chunki unga ETag sarlavhasi qo'shiladi. 401/403 uchun klass hozir yo'q — autentifikatsiya 28-qismda keladi, o'shanda qo'shiladi.

Haddan oshish ham xavfli: VazifaMatniBoshXatosi, VazifaMatniUzunXatosi, TegNotogriXatosi... Bular hammasi bitta ValidatsiyaXatosi, farqi — xatolar ichidagi joy va xabar.

Tekshirib ko'ring: Nega MetodYoqXatosi uchun kod berilmaydi va type — about:blank?

Javob

405 — umumiy HTTP holati, vazifalar-api ga xos emas. RFC 9457 bunday holatda type: "about:blank" va title sifatida standart status matnini ("Method Not Allowed") tavsiya qiladi. Domen katalogidagi kod faqat API'ning o'z ma'nosi bor xatolari uchun (royxat-toldi, versiya-mos-emas).

5. Xatoni kim tashlaydi, kim ko'radi?

5.1 Xizmat qatlami — tashlaydi

Qatlamli tuzilma darsida xizmat.ts "HTTP'ni bilmaydi" degan edik. Holat 34 da u iyerarxiyadan foydalanadi (kanon, qisqartirilgan):

ts
const topilmadi = (id: number) => new TopilmadiXatosi(
  `Vazifa ${String(id)} topilmadi`,
  "vazifa-topilmadi", "Vazifa topilmadi");

async function topilsin(id: number): Promise<Vazifa> {
  const v = await ombor.ol(id);
  if (!v) throw topilmadi(id);
  return v;
}

Halol savol: TopilmadiXatosi ichida 404 bor — demak xizmat HTTP'ni "biladi"mi? Qisman, ha. Ikki yo'l bor:

Yo'l Afzalligi Kamchiligi
Klassda status (kanon) Oddiy, bitta joy Domen va HTTP aralashadi
Klassda faqat ma'no, status — ishlovchida jadval Toza qatlamlar; CLI/navbat ham ishlatadi Ikki joyni bir xil saqlash kerak

Kanon birinchisini tanladi: bitta API, bitta transport (HTTP). NestJS qismida xuddi shunday "HTTP exception" klasslari ko'rasiz. Agar xizmat keyinroq navbat ishchisida ham ishlatilsa (27–29-qismlar), ikkinchi yo'lga o'tish mumkin.

5.2 Marshrut — o'tkazib yuboradi

Marshrutda try/catch yo'q. Express 5 da async handler'dagi throw o'zi xato ishlovchiga boradi:

ts
r.get("/:id", tekshirib({ params: IdSxemasi },
  async ({ params }, req, res) => {
    const v = await xizmat.ol(params.id); // 404 → throw
    shartliJavob(req, res, v);
  }));

Bu — Xato strategiyasi darsidagi "chegara qoidasi": xato yaratilgan joyda emas, javobni yig'adigan bitta joyda ushlanadi. O'sha joy — keyingi darsdagi global xato ishlovchi.

5.3 Asl sababni saqlash: cause

Ba'zan begona xatoni o'z turingizga aylantirasiz. Masalan, mijoz yuborgan kursor (?cursor=…) — base64 ichida JSON. Uni ochishda SyntaxError chiqsa, bu mijoz xatosi — 400:

js
class IlovaXatosi extends Error {
  operatsion = true;
  constructor(holat, xabar, sozlama = {}) {
    const c = sozlama.cause;
    super(xabar, c ? { cause: c } : undefined);
    this.name = new.target.name;
    this.holat = holat;
  }
}
class ParametrXatosi extends IlovaXatosi {
  constructor(xabar, cause) {
    super(400, xabar, { cause });
  }
}

function kursorniOch(matn) {
  try {
    return JSON.parse(atob(matn));
  } catch (e) {
    throw new ParametrXatosi("cursor buzilgan", e);
  }
}

try {
  kursorniOch("eyJ2Ijpb"); // '{"v":[' — yarim JSON
} catch (x) {
  console.log(x.name, x.holat, x.message);
  console.log("sabab:", x.cause.name);
}

Konsolda:

text
ParametrXatosi 400 cursor buzilgan
sabab: SyntaxError

Mijoz faqat "cursor buzilgan" ni ko'radi. Log esa (agar yozilsa) cause orqali asl SyntaxError ni saqlaydi. atob — base64 ni ochadigan brauzer funksiyasi (Node'da ham bor); kanon Buffer ishlatadi.

Diqqat: cause ni hech qachon javobga qo'ymang. Unda ichki fayl yo'li, SQL so'rovi yoki parol bo'lishi mumkin — keyingi darsda bunga test yozilganini ko'rasiz.

6. Turni aniqlash: instanceof va operatsion

6.1 Ishlovchi qanday ajratadi

Keyingi darsdagi ishlovchining yuragi — bitta if. Uning soddalashtirilgan modeli:

js
class IlovaXatosi extends Error {
  operatsion = true;
  constructor(holat, xabar) {
    super(xabar);
    this.name = new.target.name;
    this.holat = holat;
  }
}
class TopilmadiXatosi extends IlovaXatosi {
  constructor(xabar) { super(404, xabar); }
}

function javobniTanla(xato) {
  if (xato instanceof IlovaXatosi) {
    return { status: xato.holat, detail: xato.message };
  }
  return { status: 500, detail: "Kutilmagan xato" };
}

const holatlar = [
  new TopilmadiXatosi("Vazifa 77 topilmadi"),
  new TypeError("Cannot read properties of undefined"),
  new Error("ulanish uzildi: parol=maxfiy"),
];
for (const x of holatlar) console.log(javobniTanla(x));

Konsolda:

text
{ status: 404, detail: 'Vazifa 77 topilmadi' }
{ status: 500, detail: 'Kutilmagan xato' }
{ status: 500, detail: 'Kutilmagan xato' }

Uchinchi qatorga qarang: "parol=maxfiy" javobga tushmadi. Iyerarxiyadan tashqaridagi xato — hech qachon detail ga chiqmaydi. Bu himoya instanceof ning o'zida, alohida "yashirish" kodi kerak emas.

6.2 instanceof mi yoki operatsion maydonimi?

Ikkala tekshiruv ham bor. Farqi:

  • instanceof IlovaXatosi — xato aynan shu kod nusxasidagi klassdan. Bir loyihada yetarli.
  • xato.operatsion === true — "o'rdakka o'xshasa" tekshiruvi (duck typing). Klass boshqa paketda yoki ikki marta yuklangan bo'lsa ham ishlaydi.

Ikkinchi holat amalda uchraydi: monorepoda bir paketning ikki versiyasi node_modules da bo'lsa, ikki xil IlovaXatosi klassi paydo bo'ladi va instanceof false beradi. Kanon ishlovchida instanceof ishlatadi (bitta loyiha), operatsion maydoni esa process darajasidagi xatolar darsida va loglarda foydali. Ko'p kutubxonalar (http-errors) aynan maydon bilan ishlaydi: status va expose.

Tekshirib ko'ring: class XatoliOmbor extends XotiraOmbori testda throw new Error("disk to'ldi") qiladi. Javob statusi qanday bo'ladi va nega?

Javob
  1. Error — IlovaXatosi ning bolasi emas, demak ishlovchi uni "kutilmagan" deb oladi: mijozga umumiy matn, logga to'liq xato. Disk to'lishi haqiqatan ham API mijoz tuzata oladigan narsa emas.

7. API qadami: xato iyerarxiyasi (holat 34, 4-qism)

Holat 34 olti darsga bo'lingan: validatsiya, Zod 4, validatorlar taqqosi, bugungi iyerarxiya, global ishlovchi va process darajasidagi xatolar. Hali commit qilmang — commit oltinchi darsda.

Bugungi o'zgarishlar:

Fayl O'zgarish
src/umumiy/xatolar.ts Yangi: IlovaXatosi + 6 bola, MaydonXatosi, XatoSozlamasi
src/umumiy/http-xato.ts O'chirildi (HttpXato, validatsiyaXatosi)
src/vazifalar/xizmat.ts TopilmadiXatosi, KonfliktXatosi, ShartXatosi
src/umumiy/tekshir.ts body → ValidatsiyaXatosi, query/params → ParametrXatosi
marshrutlar .all(...) → MetodYoqXatosi

src/umumiy/xatolar.ts — to'liq (kanon holat 34; qatorlar telefon uchun Prettier bilan bo'lindi, kod va ma'no aynan; uzun izohlar bir necha qatorga o'tdi):

ts
// xatolar.ts — xato iyerarxiyasi. IlovaXatosi — KUTILGAN
// (operational) xatolar: mijozga aniq javob, log darajasi warn emas.
// Undan meros olmagan har qanday xato (TypeError, DB uzilishi…) —
// dasturchi xatosi yoki kutilmagan holat: 500, log error, tafsilot
// yashiriladi

/**
 * Validatsiya xatosi tafsiloti: qaysi joy (body.matn, query.limit) va
 * nima noto'g'ri
 */
export type MaydonXatosi = { joy: string; xabar: string };

export type XatoSozlamasi = ErrorOptions & {
  /**
   * mashina o'qiydigan kod — katalogdagi type URI shundan; yo'q
   * bo'lsa type "about:blank"
   */
  kod?: string;
  /** odam o'qiydigan qisqa sarlavha (har kod uchun bir xil) */
  sarlavha?: string;
  xatolar?: MaydonXatosi[];
  /**
   * javobga qo'shimcha HTTP sarlavhalar (Retry-After, Allow, ETag …)
   */
  httpSarlavhalar?: Record<string, string>;
};

export class IlovaXatosi extends Error {
  readonly holat: number;
  readonly kod: string | undefined;
  readonly sarlavha: string | undefined;
  readonly xatolar: MaydonXatosi[] | undefined;
  readonly httpSarlavhalar: Record<string, string>;
  /**
   * kutilgan xato — jarayon holati buzilmagan, ishlashda davom etish
   * xavfsiz
   */
  readonly operatsion = true;

  /**
   * @param xabar Problem Details `detail` — aynan shu holat haqida
   */
  constructor(
    holat: number,
    xabar: string,
    sozlama: XatoSozlamasi = {},
  ) {
    super(
      xabar,
      sozlama.cause === undefined
        ? undefined
        : { cause: sozlama.cause },
    );
    // voris klass nomi: "TopilmadiXatosi"
    this.name = new.target.name;
    this.holat = holat;
    this.kod = sozlama.kod;
    this.sarlavha = sozlama.sarlavha;
    this.xatolar = sozlama.xatolar;
    this.httpSarlavhalar = sozlama.httpSarlavhalar ?? {};
  }
}

/** 400: query yoki yo'l parametri noto'g'ri */
export class ParametrXatosi extends IlovaXatosi {
  constructor(xabar: string, xatolar?: MaydonXatosi[]) {
    super(
      400,
      xabar,
      xatolar === undefined
        ? { kod: "parametr", sarlavha: "Parametr noto'g'ri" }
        : {
            kod: "parametr",
            sarlavha: "Parametr noto'g'ri",
            xatolar,
          },
    );
  }
}

/** 422: so'rov tanasi JSON, lekin qoidaga mos emas */
export class ValidatsiyaXatosi extends IlovaXatosi {
  constructor(
    xatolar: MaydonXatosi[],
    xabar = xatolar.length === 1
      ? (xatolar[0]?.xabar ?? "")
      : `${String(xatolar.length)} ta maydon noto'g'ri`,
  ) {
    super(422, xabar, {
      kod: "validatsiya",
      sarlavha: "So'rov ma'lumoti noto'g'ri",
      xatolar,
    });
  }
}

export class TopilmadiXatosi extends IlovaXatosi {
  constructor(xabar: string, kod?: string, sarlavha?: string) {
    super(
      404,
      xabar,
      kod === undefined
        ? {}
        : sarlavha === undefined
          ? { kod }
          : { kod, sarlavha },
    );
  }
}

/**
 * 409: holat bilan to'qnashuv (ro'yxat to'lgan, so'rov hali
 * bajarilmoqda)
 */
export class KonfliktXatosi extends IlovaXatosi {
  constructor(
    xabar: string,
    sozlama: XatoSozlamasi & { kod: string },
  ) {
    super(409, xabar, sozlama);
  }
}

/** 412: If-Match versiyasi eskirgan */
export class ShartXatosi extends IlovaXatosi {
  constructor(xabar: string, joriyEtag?: string) {
    super(
      412,
      xabar,
      joriyEtag === undefined
        ? {
            kod: "versiya-mos-emas",
            sarlavha: "Vazifa o'zgarib ketgan",
          }
        : {
            kod: "versiya-mos-emas",
            sarlavha: "Vazifa o'zgarib ketgan",
            httpSarlavhalar: { ETag: joriyEtag },
          },
    );
  }
}

export class MetodYoqXatosi extends IlovaXatosi {
  constructor(metod: string, allow: string) {
    super(405, `${metod} bu manzilda yo'q`, {
      httpSarlavhalar: { Allow: allow },
    });
  }
}

Yuqoridagi «Bolalar» bo'limida soddalashtirilgan ShartXatosi va ParametrXatosi bilan farqqa qarang: kanonda exactOptionalPropertyTypes tufayli undefined qiymatli kalit yozilmaydi — shuning uchun ternar tarmoqlar.

Kanon tekshir() funksiyasi (holat 34) Zod natijasini shunday ajratadi:

ts
const xatolar = xatolarga(joy, n.error);
if (joy === "body") throw new ValidatsiyaXatosi(xatolar);
throw new ParametrXatosi(xatolar[0]?.xabar ?? "noto'g'ri", xatolar);

Tana — 422, manzil (query, params) — 400. Sabab: manzil noto'g'ri bo'lsa, so'rovning o'zi noto'g'ri tuzilgan; tana noto'g'ri bo'lsa — so'rov to'g'ri, mazmun qoidaga zid.

Tashqaridan javoblar o'zgarmadi (type, kod, title holat 24 dagidek). Bu — refaktoring: ichki tuzilma o'zgaradi, tashqi xulq yo'q. Kanon jonli chiqishi (tekshiruv-34.txt):

text
$ curl $A/v1/vazifalar/abc
{"type":"https://vazifalar-api.example/xatolar/parametr","title":"Parametr noto'g'ri","status":400,"detail":"id — musbat butun son","instance":"/v1/vazifalar/abc","kod":"parametr","xatolar":[{"joy":"params.id","xabar":"id — musbat butun son"}],"sorovId":"100de325-cde0-4082-8bd8-f8f8cc9fb141"}

Biz ham holat 34 ni npm ci dan keyin ishga tushirib, 404 ni so'radik:

text
$ curl $A/v1/vazifalar/77
{"type":"https://vazifalar-api.example/xatolar/vazifa-topilmadi","title":"Vazifa topilmadi","status":404,"detail":"Vazifa 77 topilmadi","instance":"/v1/vazifalar/77","kod":"vazifa-topilmadi","sorovId":"2a6ec15f-dbde-4721-a14b-d3f9f3f001cd"}

Kanon test/sxema.test.ts iyerarxiyani ham tekshiradi: tekshir() tashlagan xato instanceof IlovaXatosi, name — "ValidatsiyaXatosi" yoki "ParametrXatosi".

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Logda hamma xato Error: … name qo'yilmagan Otada this.name = new.target.name
instanceof false, garchi klass to'g'ri bo'lsa ham Klass ikki nusxada yuklangan operatsion yoki kod maydoni bilan tekshiring
Mijoz detail da SQL yoki fayl yo'lini ko'rdi Begona xato xabari javobga ketgan Faqat IlovaXatosi ning message i javobga
Asl sabab logda yo'q cause berilmagan new ParametrXatosi(x, e) → { cause }
30 ta klass, har biri bir marta ishlatilgan Har maydon uchun klass Bitta ValidatsiyaXatosi + xatolar ro'yxati
throw "topilmadi" Satr tashlangan — stack yo'q Doim Error yoki uning bolasi

9. Mashqlar

Yechimlar kurs/mashqlar/25/32-xato-turlari/ da.

1-mashq (oson): turini ayting

Har holat uchun: operatsion (o) yoki kutilmagan (k), va status.

  • POST tanasida noma'lum maydon "ortiqcha": 1 — [:o], [:422]
  • JSON.stringify aylanma obyektda yiqildi — [:k], [:500]
  • PATCH /v1/vazifalar/5, If-Match: "2", vazifa 3-versiyada — [:o], [:412]
  • ?sort=parol — [:o], [:400]
Yechim
  • o, 422 — tana mazmuni qoidaga zid (strictObject).
  • k, 500 — kodimiz ma'lumotni noto'g'ri yig'gan; mijoz buni tuzata olmaydi.
  • o, 412 — versiya mos emas, mijoz qayta o'qishi kerak.
  • o, 400 — query parametri noto'g'ri (oq ro'yxatda yo'q).

2-mashq (o'rta): HajmXatosi

Iyerarxiyaga 413 uchun HajmXatosi(chegaraKb) qo'shing: kod — "hajm", sarlavha — "Tana juda katta", xabar — "16 KB dan katta". javobniTanla (darsdagidek) uni to'g'ri qaytarsin, va name qo'lda yozilmasin.

Yechim
js
class IlovaXatosi extends Error {
  operatsion = true;
  constructor(holat, xabar, sozlama = {}) {
    super(xabar);
    this.name = new.target.name;
    this.holat = holat;
    this.kod = sozlama.kod;
    this.sarlavha = sozlama.sarlavha;
  }
}
class HajmXatosi extends IlovaXatosi {
  constructor(chegaraKb) {
    super(413, `${chegaraKb} KB dan katta`, {
      kod: "hajm",
      sarlavha: "Tana juda katta",
    });
  }
}
function javobniTanla(xato) {
  if (xato instanceof IlovaXatosi) {
    return { status: xato.holat, title: xato.sarlavha,
      detail: xato.message };
  }
  return { status: 500, title: "Internal Server Error" };
}
const x = new HajmXatosi(16);
console.log(x.name);
console.log(javobniTanla(x));

Konsolda:

text
HajmXatosi
{ status: 413, title: 'Tana juda katta', detail: '16 KB dan katta' }

name otadagi new.target.name dan keldi. Kanonda 413 alohida klass emas: uni express.json (body-parser) o'zi tashlaydi va ishlovchi http-errors sifatida taniydi — bu keyingi darsda.

3-mashq (qiyin): ikki nusxali klass

Ikki "paket" bir xil nomli klassni alohida e'lon qilgan holatni modellang: paketA.IlovaXatosi va paketB.IlovaXatosi. B'dan tashlangan xatoni A'ning instanceof i bilan tekshiring — natijani ko'ring. Keyin operatsionmi(x) funksiyasini yozing: u klassga emas, maydonlarga qarasin (operatsion === true va holat — 400–499 oralig'idagi butun son).

Yechim
js
function paketYarat() {
  return class IlovaXatosi extends Error {
    operatsion = true;
    constructor(holat, xabar) {
      super(xabar);
      this.name = new.target.name;
      this.holat = holat;
    }
  };
}
const paketA = { IlovaXatosi: paketYarat() };
const paketB = { IlovaXatosi: paketYarat() };

const x = new paketB.IlovaXatosi(404, "topilmadi");
console.log("A instanceof:", x instanceof paketA.IlovaXatosi);
console.log("B instanceof:", x instanceof paketB.IlovaXatosi);

function operatsionmi(x) {
  return typeof x === "object" && x !== null &&
    x.operatsion === true && Number.isInteger(x.holat) &&
    x.holat >= 400 && x.holat <= 499;
}
console.log(operatsionmi(x));
console.log(operatsionmi(new TypeError("t")));
console.log(operatsionmi({ operatsion: true, holat: 503 }));

Konsolda:

text
A instanceof: false
B instanceof: true
true
false
false

Ikki klass nomi bir xil, lekin ular ikki xil obyekt — instanceof prototip zanjiriga qaraydi, nomga emas. holat ni 400–499 bilan cheklash — xavfsizlik: tasodifan operatsion maydoni bor 5xx xato detail ni ochib qo'ymasin.

4-mashq: API qadami

src/umumiy/xatolar.ts ni yarating (IlovaXatosi + 6 bola). http-xato.ts ni o'chiring va TypeScript ko'rsatgan har joyni yangi klasslarga o'tkazing: xizmat.ts (404, 409, 412), tekshir.ts (422, 400), marshrutlardagi .all() (405). npm run tip — 0 xato. Testlar o'zgarmasdan o'tishi kerak — tashqi javob bir xil. Commit qilmang.

Yechim

Kod — «API qadami» bo'limidagi to'liq xatolar.ts. Tekshiruv tartibi: http-xato.ts ni o'chirgach, npm run tip har eski importni ko'rsatadi — kompilyator o'zgarish ro'yxatini beradi. Testlar o'zgarmasdan o'tsa, refaktoring xulqni buzmagan. Kesim tekshiruvi: tip 0, lint 0, testlar 35/35 — biz ishga tushirdik (Node 24.21.0). Kesim holat 34 nusxasidan yasaldi: xato-ishlovchi.ts, jarayon.ts va uning testi olib tashlandi, ilova.ts dagi ishlovchi IlovaXatosi ni taniydi. 36-chisi — jarayon xatolari darsidagi test.

10. Real ishda

  • Kutubxonalar: http-errors (Express ekotizimi: createError(404), status, expose), NestJS'ning HttpException, NotFoundException — aynan shu g'oya. Ularni o'qiy olasiz.
  • Monitoring: Sentry kabi xizmatlar (34-qism) operatsion xatolarni ignoreErrors yoki beforeSend bilan filtrlaydi — name va kod shu yerda kerak bo'ladi.
  • Intervyu savollari: "Operatsion va dasturchi xatosining farqi?", "Xato klasslarini qanday tuzasiz?", "Nega 500 javobida xato xabarini qaytarmaysiz?"

Xulosa

  • Operatsion xato — mijozga aniq sabab aytib bo'ladigan holat; qolgani — 500 va log.
  • Bitta ota klass (IlovaXatosi): holat, kod, sarlavha, xatolar, httpSarlavhalar, operatsion = true.
  • this.name = new.target.name — nom avtomatik, har bolada takrorlanmaydi.
  • Bola klass — alohida ishlov kerak bo'lgan vaziyat uchun; maydon xatolari — bitta ValidatsiyaXatosi ichida.
  • Iyerarxiyadan tashqaridagi xato hech qachon detail ga chiqmaydi; asl sabab cause da, logda.

Keyingi dars: Global error handler — 4 argumentli middleware hamma xatoni bir joyda Problem Details'ga aylantiradi; Express'ning o'z ishlovchisi stack'ni mijozga qanday ko'rsatib qo'yishini o'z ko'zimiz bilan ko'ramiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Express xato turlari va custom error iyerarxiyasi: operatsion xato va dasturchi xatosi — IlmHamroh