İçeriğe geç
academia.sh

Ders 15 / 20

Hata Yönetimi Stratejisi

İşletimsel hata ile programcı hatasının ayrımı, hata nedenlerinin zincirlenmesi, eşzamansız hataların yakalanamayan biçimleri ve son çare kancalarının doğru kullanımı.

İçindekiler

Önceki ders, yapılandırma hatasının sürecin açılmasını engellemesi gerektiğini söyledi. Çalışma sırasında ortaya çıkan her hata aynı davranışı hak etmez: bir isteğin bozuk gövdesi hizmeti kapatmamalı, ama bellekte bozulmuş bir durum onu kapatmalıdır.

Bu dersin sorusu şudur: hangi hata yakalanır, hangisi sürecin sonlanmasına yol açar? Ayrım tek bir ölçüte dayanır ve stratejinin tamamı ondan çıkar.

İki Hata Sınıfı

İşletimsel hata, doğru yazılmış bir programın çalışma koşullarında karşılaşmayı beklediği durumdur: dosya yok, ağ ulaşılamıyor, istemci geçersiz veri gönderdi, disk doldu. Bunlar programın kusuru değildir; öngörülmüş ve ele alınmıştır.

Programcı hatası, kodun kendi sözleşmesini ihlal etmesidir: tanımsız bir değerin alanına erişmek, zorunlu bir argümanı geçirmeyi unutmak, bir sayı beklenen yere nesne vermek. Bunlar öngörülmemiştir ve programın o andaki durumu hakkındaki varsayımların yanlış olduğunu gösterir.

Ayrımın kuralı şudur: işletimsel hata ele alınır, programcı hatası ele alınmaz. İkincisini yakalayıp devam etmek, bozulmuş bir durumla çalışmaya devam etmek demektir; ne kadar sürdüğü belli olmayan ve tanısı çok daha zor bir arıza üretir.

Ayrımın kodda görünür olması için işletimsel hatalar kendi tipiyle işaretlenir:

// hata-sinifi.mjs
export class IsletimselHata extends Error {
  constructor(ileti, { kod, durum = 500, cause } = {}) {
    super(ileti, { cause });
    this.name = 'IsletimselHata';
    this.kod = kod;
    this.durum = durum;
    this.isletimsel = true;
  }
}

export function isletimselMi(hata) {
  return hata instanceof IsletimselHata && hata.isletimsel === true;
}

Nedeni Zincirlemek

Alt katmandaki bir hata üst katmana taşınırken çevrilir: üst katman kendi diliyle konuşan bir hata fırlatır, ama asıl nedeni kaybetmez. cause seçeneği bu bağı kurar.

// hata-dene.mjs
import { readFile } from 'node:fs/promises';
import { IsletimselHata, isletimselMi } from './hata-sinifi.mjs';

async function olcumleriOku(yol) {
  try {
    return await readFile(yol, 'utf8');
  } catch (hata) {
    if (hata.code === 'ENOENT') {
      throw new IsletimselHata(`olcum dosyasi yok: ${yol}`, {
        kod: 'OLCUM_DOSYASI_YOK', durum: 503, cause: hata,
      });
    }
    throw hata;
  }
}

for (const yol of ['olcumler.ndjson', 'yok.ndjson']) {
  try {
    const metin = await olcumleriOku(yol);
    console.log(`${yol}: ${metin.trim().split('\n').length} satir`);
  } catch (hata) {
    console.log(`${yol}: isletimsel mi = ${isletimselMi(hata)}, kod = ${hata.kod}, durum = ${hata.durum}`);
    console.log(`  asil neden: ${hata.cause.code}`);
  }
}

// Programci hatasi: sozlesme ihlali, yakalanip yutulmaz
try {
  null.deger;
} catch (hata) {
  console.log(`programci hatasi: isletimsel mi = ${isletimselMi(hata)}, tur = ${hata.constructor.name}`);
}
node hata-dene.mjs
olcumler.ndjson: 12 satir
yok.ndjson: isletimsel mi = true, kod = OLCUM_DOSYASI_YOK, durum = 503
  asil neden: ENOENT
programci hatasi: isletimsel mi = false, tur = TypeError

Üç ayrıntı önemlidir.

Yalnızca tanınan hata kodu çevriliyor; geri kalanı olduğu gibi yukarı fırlatılıyor. Bütün hataları tek bir tipe çevirmek, bilinmeyen arızaları bilinen bir kategoriye sokarak gizler.

Çevrilen hata kendi kodunu taşıyor. Çağıran, ileti metnini ayrıştırmadan karar verebilir; ileti metni değişse de kod sabit kalır.

Asıl neden erişilebilir durumda. Tanı sırasında zincirin en altındaki sistem çağrısı hatasına ulaşmak, arızayı bulmanın en kısa yoludur.

Hizmette Karşılığı

HTTP Sunucusu dersindeki hata yakalayıcı her hatayı 400 olarak bildiriyordu. Ayrım kurulduktan sonra doğru eşleme yapılabilir:

// hata-sunucu.mjs
import { createServer } from 'node:http';
import { IsletimselHata, isletimselMi } from './hata-sinifi.mjs';
import { satirdanKayit } from './ozetleyici.mjs';

function hataYaniti(hata, yanit) {
  if (isletimselMi(hata)) {
    yanit.writeHead(hata.durum, { 'content-type': 'application/json' });
    yanit.end(JSON.stringify({ hata: hata.message, kod: hata.kod }) + '\n');
    return;
  }
  // Programci hatasi: ayrinti disariya verilmez, iceride tam kaydedilir
  process.stderr.write(`programci hatasi: ${hata.stack.split('\n')[0]}\n`);
  yanit.writeHead(500, { 'content-type': 'application/json' });
  yanit.end(JSON.stringify({ hata: 'ic hata', kod: 'IC_HATA' }) + '\n');
}

const sunucu = createServer(async (istek, yanit) => {
  try {
    if (istek.url === '/olcum') {
      const parcalar = [];
      for await (const p of istek) parcalar.push(p);
      const govde = Buffer.concat(parcalar).toString('utf8');
      try {
        satirdanKayit(govde.trim());
      } catch (hata) {
        throw new IsletimselHata('gecersiz olcum kaydi', {
          kod: 'GECERSIZ_KAYIT', durum: 400, cause: hata,
        });
      }
      yanit.writeHead(201, { 'content-type': 'application/json' });
      yanit.end('{"alinan":1}\n');
      return;
    }
    if (istek.url === '/coken') {
      null.deger;                       // bilerek programci hatasi
    }
    throw new IsletimselHata('bulunamadi', { kod: 'YOL_YOK', durum: 404 });
  } catch (hata) {
    hataYaniti(hata, yanit);
  }
});

sunucu.listen(8791, '127.0.0.1', () => console.log('dinleniyor'));

Sunucu çalışırken dört istek gönderelim:

curl -s -X POST --data-binary 'bozuk' -w 'durum: %{http_code}\n' http://127.0.0.1:8791/olcum
{"hata":"gecersiz olcum kaydi","kod":"GECERSIZ_KAYIT"}
durum: 400
curl -s -w 'durum: %{http_code}\n' http://127.0.0.1:8791/yok
{"hata":"bulunamadi","kod":"YOL_YOK"}
durum: 404
curl -s -w 'durum: %{http_code}\n' http://127.0.0.1:8791/coken
{"hata":"ic hata","kod":"IC_HATA"}
durum: 500

Son çağrıda sunucunun uçbiriminde şu satır yazıldı:

programci hatasi: TypeError: Cannot read properties of null (reading 'deger')

İşletimsel hatanın iletisi istemciye veriliyor, programcı hatasınınki verilmiyor. Bu bir gizleme alışkanlığı değil, iki gerekçeye dayanan bir karardır: yığıt izi ve iç ileti, dosya yollarını ve kod yapısını dışarıya sızdırır; ayrıca istemcinin yapabileceği bir şey yoktur — 500 yanıtının tek anlamlı yanıtı yeniden denemek veya bildirmektir.

Eşzamansız Hatanın Yakalanamadığı Yerler

try/catch yalnızca kendi eşzamanlı gövdesini ve beklenen sözleri kapsar. Bir geri çağrının içinde fırlatılan hata bu kapsamın dışındadır.

// yakalanmayan.mjs
import { readFile } from 'node:fs/promises';

// try/catch, geri cagriya ertelenen hatayi yakalamaz
try {
  setTimeout(() => { throw new Error('geri cagri icinde firlatildi'); }, 0);
} catch (hata) {
  console.log('bu satir calismaz');
}

// Beklenmeyen reddetme
readFile('yok.ndjson', 'utf8');

console.log('senkron akis bitti');
node yakalanmayan.mjs

Çıktının ilk satırı senkron akis bitti olur; ardından yakalanmayan hata yazılır ve süreç sıfırdan farklı bir kodla sonlanır. bu satir calismaz satırı hiç yazılmaz: setTimeout çağrısı hemen döndü, hata ise çok sonra, döngünün zamanlayıcı aşamasında fırlatıldı — o anda try bloğu çoktan bitmişti.

Aynı dosyadaki ikinci sorun daha sinsidir. readFile çağrısı beklenmediği için döndürdüğü sözün reddi hiçbir yerde ele alınmaz. Bu, işlenmemiş reddetmedir ve varsayılan davranışı süreci sonlandırmaktır:

// reddetme.mjs
import { readFile } from 'node:fs/promises';

readFile('yok.ndjson', 'utf8');     // donen soz beklenmiyor, hatasi da islenmiyor
console.log('senkron akis bitti');
node reddetme.mjs > /dev/null; echo "cikis kodu: $?"
cikis kodu: 1

Sürecin sonlanması sert görünür ama doğrudur: sessizce yutulan bir reddetme, verinin yazıldığı sanılırken yazılmamış olmasına yol açar. Kural şudur: söz döndüren her çağrı ya beklenir ya da reddi açıkça ele alınır.

Son Çare Kancaları

İki kanca, yakalanmamış hataları programın eline verir:

// son-care.mjs
process.on('uncaughtException', (hata, koken) => {
  process.stderr.write(`olumcul: ${hata.message} (koken: ${koken})\n`);
  process.exit(1);            // durum bozulmus sayilir: yeniden baslat
});

process.on('unhandledRejection', (neden) => {
  process.stderr.write(`islenmemis reddetme: ${neden.message ?? neden}\n`);
  process.exit(1);
});

setTimeout(() => { throw new Error('gec gelen hata'); }, 0);
console.log('kanca kuruldu');
node son-care.mjs; echo "cikis kodu: $?"
kanca kuruldu
olumcul: gec gelen hata (koken: uncaughtException)
cikis kodu: 1

Bu kancaların tek meşru kullanımı, sürecin ölmeden önce arızayı kaydetmesidir. Kancayı kurup çalışmaya devam etmek, çalışma zamanının varsayılan davranışını bozar ve bilinmeyen bir durumda ilerlemeye izin verir.

Kancanın içinde uzun iş yapılmaz. Süreç zaten şüpheli bir durumdadır; kancada eşzamansız bir kütük yazımı başlatmak, yazının tamamlanacağının güvencesi yoktur. Kısa ve eşzamanlı bir satır yazıp çıkmak en güvenlisidir.

Süreç öldükten sonra hizmetin ayakta kalması, sürecin kendi sorumluluğu değildir. Bu işi, süreci yeniden başlatan dış bir denetçi üstlenir; Süreç Yönetimi ve Sağlamlık dersinde ele alınacak.

Özet

  • İşletimsel hata öngörülmüş bir çalışma koşuludur ve ele alınır; programcı hatası sözleşme ihlalidir ve ele alınmaz.
  • İşletimsel hatalar kendi tipleriyle işaretlenir ve kod alanı taşır; çağıran karar verirken ileti metnini ayrıştırmaz.
  • Hata çevrilirken cause ile asıl neden korunur; yalnızca tanınan durumlar çevrilir, geri kalanı olduğu gibi yukarı fırlatılır.
  • Geri çağrı içinde fırlatılan hata ve beklenmeyen sözün reddi try/catch kapsamına girmez; ikincisinin varsayılan sonucu sürecin sonlanmasıdır.
  • Son çare kancaları yalnızca kaydetmek ve çıkmak için kullanılır; kancayı kurup devam etmek bilinmeyen bir durumda ilerlemektir.

Sonraki Adım

Hataların kaydedilmesinden birkaç kez söz edildi ama nereye ve hangi biçimde yazılacağı belirlenmedi. Serbest metin bir ileti, arızayı arayan kişinin işine yaramaz: arama, süzme ve sayma gerektiren bir kayıt biçimi gerekir. Sonraki ders yapılandırılmış günlüklemeyi kurar ve düzey yönetimini ekler.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat