Mert Özen Her Satırda Daha İleri
Frontend JavaScript Orta — #06

JavaScript Hata Yönetimi: try/catch, Kendi Error Class'ın ve cause ile Hata Zinciri

Mert Özen 3 Kas 2026 10 dk 5 görüntülenme
JavaScript Hata Yönetimi: try/catch, Kendi Error Class'ın ve cause ile Hata Zinciri

try/catch/finally ve throw'u, yerleşik hata türlerini, extends Error ile kendi hata class'ını, ES2022'nin cause zincirini, Safari'de henüz olmayan Error.isError'ı ve hataları kullanıcıya düzgün göstermeyi öğreniyoruz.

Sürüm notu: Bu yazıdaki kodlar ES2026 ve güncel tarayıcılar (Chrome, Edge, Firefox, Safari) içindir.

Önce Tahmin Et

Bir return ve bir finally aynı fonksiyonda karşılaşıyor:

function test() {
  try {
    return "try";
  } finally {
    console.log("finally");
  }
}

console.log(test());

Bu kod konsola ne yazdırır, hangi sırayla? Okumaya devam etmeden önce bir tahmin yap. return fonksiyonu bitirir diye öğrendik; peki finally'ye sıra gelir mi? Cevabı try/catch/finally'yi anlatırken vereceğiz.

try, catch ve finally

Bir binadaki yangın alarmını düşün. Bir katta duman algılandığında alarm çalar, o kattaki iş hemen durur ve insanlar yangın merdiveninden aşağı iner. Aşağıda biri ne yapılacağını biliyorsa (toplanma noktası, itfaiye) her şey kontrol altındadır. Kimse bilmiyorsa kaos olur. JavaScript'te hata da böyledir. Bir hata fırlatıldığında engine o anda çalışan fonksiyonu bırakır ve call stack'te yukarı doğru, kendisini yakalayacak en yakın catch'i arar. Bulamazsa hata konsola kırmızı bir mesaj olarak düşer ve o script'in geri kalanı çalışmaz.

Daha önce JSON yazısında try/catch'e kısaca dokunmuştuk. Şimdi tamamını görelim. Bunu tarayıcının konsoluna yapıştırıp dene:

try {
  const data = JSON.parse("{ broken json");
  console.log(data); // hiç çalışmaz
} catch (error) {
  console.log(error.name);    // "SyntaxError"
  console.log(error.message); // metin tarayıcıya göre değişir
} finally {
  console.log("runs no matter what");
}

console.log("the program carries on");

try bloğundaki kod hata verdiği anda kalan satırlar atlanır ve kontrol catch'e geçer. catch'in parantezindeki değişken fırlatılan hatanın kendisidir. finally ise hata olsa da olmasa da en sonda çalışır; açılan bir yükleniyor göstergesini kapatmak ya da bir butonu tekrar aktif etmek gibi temizlik işleri için idealdir. Hata object'ine ihtiyacın yoksa ES2019'dan beri parantezi tamamen atıp sadece catch { ... } yazabilirsin.

Şimdi baştaki soruya dönelim. Konsola önce "finally", sonra "try" yazılır. return "try" satırı dönüş değerini hazırlar ama fonksiyon hemen çıkmaz; engine önce finally'yi çalıştırır, ardından bekleyen değeri döndürür. finally gerçekten "ne olursa olsun" demektir. Hatta öyle ki finally'nin içine kendi return'ünü yazarsan, o try'daki dönüş değerini ve fırlatılmış bir hatayı bile ezer. Bu yüzden finally'de return kullanma.

throw ve Error Object'i

Hataları sadece yakalamayız, gerektiğinde kendimiz de fırlatırız. Bir fonksiyon, işini yapamayacağı bir durumla karşılaştığında yanlış bir değer döndürüp sessizce devam etmek yerine yüksek sesle "bunu yapamam" demelidir. Bunu throw ile yaparsın:

function withdraw(balance, amount) {
  if (typeof amount !== "number" || Number.isNaN(amount)) {
    throw new TypeError("Amount must be a number");
  }
  if (amount > balance) {
    throw new RangeError(`Insufficient balance: you have ${balance} USD, asked for ${amount} USD`);
  }
  return balance - amount;
}

try {
  withdraw(100, 250);
} catch (error) {
  console.log(error.name);    // "RangeError"
  console.log(error.message); // "Insufficient balance: you have 100 USD, asked for 250 USD"
  console.log(error.stack);   // hatanın oluştuğu yer
}

Bir hata object'inin üç önemli property'si var. name hatanın türünü, message ne olduğunu söyler. stack ise hatanın hangi fonksiyonda, hangi satırda oluştuğunu ve oraya hangi çağrılardan geçilerek gelindiğini gösterir. stack standartta tanımlı değildir ama bütün tarayıcılar destekler ve hata ayıklarken en iyi dostundur.

JavaScript aslında her şeyi fırlatmana izin verir: throw "something went wrong" da çalışır. Ama string'in stack'i yoktur, türünü ayırt edemezsin ve hatayı kim yakalarsa elinde sadece bir metin kalır. Her zaman bir Error object'i fırlat.

Yerleşik Hata Türleri

Engine'in kendisi de hata fırlatır ve bunlar farklı türlerdedir. Hepsi Error'dan türer. En sık göreceklerin şunlar. TypeError, bir değer üzerinde onun türüne uymayan bir işlem yapıldığında çıkar: null'ın property'sini okumak ya da fonksiyon olmayan bir şeyi çağırmak gibi. ReferenceError, tanımlanmamış bir değişkene ya da TDZ'deki bir let'e erişildiğinde gelir. RangeError, bir değer izin verilen aralığın dışındaysa, SyntaxError ise kod ya da JSON.parse'a verilen metin dil kurallarına uymuyorsa fırlatılır.

Daha nadir olanlar da var: decodeURIComponent bozuk bir girdiyle karşılaşınca URIError verir, EvalError ise eski bir kalıntıdır ve nadiren karşına çıkar. ES2021 ile gelen AggregateError birden fazla hatayı tek pakette taşır; onu promise'lerle birlikte ilerleyen yazılarda göreceğiz. Türü instanceof ile ayırt edersin:

function tryRun(fn) {
  try {
    fn();
  } catch (error) {
    if (error instanceof TypeError) {
      console.log("Type error:", error.message);
    } else if (error instanceof RangeError) {
      console.log("Range error:", error.message);
    } else {
      throw error; // bilinmeyen hata, yukarı aktar
    }
  }
}

tryRun(() => null.length);              // Type error: ...
tryRun(() => new Array(-1));            // Range error: Invalid array length
tryRun(() => decodeURIComponent("%"));  // URIError, yeniden fırlatıldı

Son satır konsolda kırmızı bir URIError gösterecek; bu bilerek yapıldı. tryRun tanımadığı bir hatayla karşılaştı ve onu yutmak yerine yeniden fırlattı. Bu önemli bir alışkanlık: sadece ne yapacağını bildiğin hataları yakala, gerisini yukarıya bırak.

Kendi Error Class'ın

Yerleşik türler engine'in hataları için yeterli ama uygulamanın kendi hataları için genelde daha anlamlı isimler istersin: doğrulama hatası, bulunamadı hatası, yetki hatası. Bunun için Error'dan türeyen kendi class'ını yazarsın. Class syntax'ı bir sonraki yazıda ayrıntılı göreceğiz; şimdilik sadece kalıbı kullanıyoruz:

class ValidationError extends Error {
  constructor(field, message) {
    super(message);
    this.name = "ValidationError";
    this.field = field;
  }
}

function validateEmail(email) {
  if (!email.includes("@")) {
    throw new ValidationError("email", "Please enter a valid email address");
  }
  return email;
}

try {
  validateEmail("ada.example.com");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log(`${error.field}: ${error.message}`); // "email: Please enter a valid email address"
  } else {
    throw error;
  }
}

Üç satırın her biri bir iş görüyor. super(message), Error'un kendi kurulumunu yapar; message ve stack buradan gelir. this.name satırı olmazsa konsolda hatanın adı yine "Error" görünür. field ise bizim eklediğimiz bilgi; formda hangi alanın altına kırmızı mesaj yazacağımızı buradan anlarız. Yakalayan taraf da artık instanceof ValidationError ile kendi hatasını diğerlerinden kolayca ayırır.

cause ile Hata Zinciri

Gerçek uygulamalarda hatalar katman katman oluşur. Ayar dosyasını okuyan bir fonksiyon JSON.parse'tan bir SyntaxError alır. Bunu doğrudan yukarı fırlatırsan, üstteki kod "ayarlar yüklenemedi" demek yerine anlamsız bir JSON mesajıyla karşılaşır. Yutarsan da asıl sebep kaybolur. ES2022 ile gelen cause seçeneği ikisini birden yapmanı sağlar: anlamlı yeni bir hata fırlatırsın ve asıl hatayı onun içine koyarsın.

function loadSettings(text) {
  try {
    return JSON.parse(text);
  } catch (error) {
    throw new Error("Could not load settings", { cause: error });
  }
}

try {
  loadSettings("{ theme: dark }");
} catch (error) {
  console.log(error.message);       // "Could not load settings"
  console.log(error.cause.name);    // "SyntaxError"
  console.log(error.cause.message); // JSON.parse'ın orijinal mesajı
}

Bir zincir gibi düşün: her halka kendi seviyesinde anlamlı bir mesaj taşır, cause ise bir alttaki halkayı tutar. Kullanıcıya en üstteki mesajı gösterirsin, hata kaydına bütün zinciri yazarsın. Kendi Error class'ında da cause'u desteklemek için constructor'a bir options parametresi ekleyip super(message, options) demen yeterli.

Error.isError: Bu Gerçekten Bir Hata mı?

Bir değerin hata olup olmadığını genelde instanceof Error ile kontrol ederiz. Bu çoğu zaman işe yarar ama iki zayıf noktası var. Bir iframe'den gelen hata, o iframe'in kendi Error'undan türediği için ana sayfadaki instanceof Error kontrolünden false döner. Üstelik instanceof kandırılabilir; prototype'ı Error.prototype olan sıradan bir object de testi geçer. ES2026 ile gelen Error.isError, değerin gerçekten engine'in oluşturduğu bir hata object'i olup olmadığına bakar:

const fake = Object.create(Error.prototype);
console.log(fake instanceof Error);              // true
console.log(Error.isError(fake));                // false
console.log(Error.isError(new TypeError("x")));  // true
console.log(Error.isError({ message: "x" }));    // false

Burada önemli bir not var: Error.isError Chrome ve Edge 134 ile Firefox 138'den beri çalışıyor ama Safari henüz desteklemiyor. Baseline'da "sınırlı" durumda. Bütün kullanıcılarına hizmet veren bir kodda şimdilik küçük bir yedekle kullan:

const isError = (value) =>
  typeof Error.isError === "function" ? Error.isError(value) : value instanceof Error;

console.log(isError(new RangeError("x"))); // true

Hataları Kullanıcıya Göstermek ve Global Yakalama

Hatayı yakalamak işin yarısı; diğer yarısı onu kullanıcıya doğru biçimde anlatmak. Kullanıcı TypeError: Cannot read properties of undefined görmek istemez, ne yapması gerektiğini bilmek ister. Geliştirici ise tam tersine bütün ayrıntıyı ister. Bu yüzden kullanıcıya sade bir mesaj gösterir, ayrıntıyı konsola ya da bir hata kaydına yazarsın. stack'i asla ekrana basma.

Tarayıcı, hiçbir catch'in yakalamadığı hatalar için window üzerinde bir error event'i tetikler. Bu son güvenlik ağıdır: hatayı düzeltmez ama kullanıcıyı boş bir ekranla baş başa bırakmamanı sağlar. Bu küçük HTML dosyasını kaydedip tarayıcıda aç, önce yaş alanını boş bırakıp Check butonuna bas, sonra Broken button'a tıkla:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Error messages</title>
  <style>.error { color: #b00020; }</style>
</head>
<body>
  <label>Age: <input id="age" type="text"></label>
  <button id="check">Check</button>
  <button id="broken">Broken button</button>
  <p id="message"></p>
  <script>
    class ValidationError extends Error {
      constructor(message) {
        super(message);
        this.name = "ValidationError";
      }
    }

    function parseAge(text) {
      const age = Number(text);
      if (text.trim() === "" || Number.isNaN(age)) {
        throw new ValidationError("Please enter your age in digits.");
      }
      if (age < 0 || age > 130) {
        throw new ValidationError("Age must be between 0 and 130.");
      }
      return age;
    }

    const output = document.getElementById("message");

    document.getElementById("check").addEventListener("click", () => {
      try {
        const age = parseAge(document.getElementById("age").value);
        output.className = "";
        output.textContent = `Thanks, you are ${age} years old.`;
      } catch (error) {
        if (!(error instanceof ValidationError)) throw error;
        output.className = "error";
        output.textContent = error.message;
      }
    });

    document.getElementById("broken").addEventListener("click", () => {
      notDefinedAnywhere(); // ReferenceError, kimse yakalamıyor
    });

    window.addEventListener("error", (event) => {
      console.log("Uncaught error:", event.error);
      output.className = "error";
      output.textContent = "Something unexpected went wrong. Please reload the page and try again.";
    });
  </script>
</body>
</html>

Doğrulama hataları beklenen hatalardır ve kendi catch'lerinde, alanın yanında anlaşılır bir mesajla karşılanır. Bozuk butondaki ReferenceError ise beklenmeyen bir hatadır; onu global listener yakalar. Mesajı DOM'a textContent ile yazdığımıza da dikkat et; hata mesajının içinde kullanıcıdan gelen bir metin olabilir ve innerHTML burada kapıyı XSS'e açar.

Eski kodlarda aynı işi yapan window.onerror = (message, source, line, column, error) => { ... } atamasını da görürsün. Promise'lerde yakalanmamış hatalar ise ayrı bir event'le, unhandledrejection ile gelir; promise'leri gördüğümüzde ona da döneceğiz.

Sık Düşülen Tuzaklar

Boş catch. catch {} yazıp hatayı yutmak, alarmın kablosunu kesmek gibidir. Sorun ortadan kalkmaz, sadece duymazsın. Gerçekten görmezden gelmen gereken bir durum yoksa en azından console.error ile kaydet.

Her şeyi tek bir try'a sarıp her hatayı aynı sanmak. Bütün kodu sarmalayıp her hatada "geçersiz giriş" demek, gerçek bir yazılım hatasını kullanıcı hatası gibi gösterir. Türü kontrol et, tanımadığını yeniden fırlat.

Zamanlayıcının içindeki hatayı dışarıdan yakalamaya çalışmak. try { setTimeout(() => { throw new Error("late") }, 0); } catch {} hiçbir şey yakalamaz. Callback çalıştığında try bloğu çoktan bitmiştir. Hata yakalanacaksa try/catch callback'in içinde olmalı. Asenkron hataların nasıl yönetildiğini promise ve async/await yazılarında göreceğiz.

Normal akışı hatayla yönetmek. Aranan kullanıcının bulunamaması uygulamanın olağan bir durumuysa null döndürmek çoğu zaman yeterlidir. Hata, gerçekten istisnai durumlar için olmalı; yoksa her çağrıyı try'a sarmak zorunda kalırsın.

Küçük Bir Deneme

★☆☆ Çalıştırmadan önce tahmin et: try { console.log("a"); throw new Error("b"); console.log("c"); } catch (e) { console.log(e.message); } finally { console.log("d"); } Hangi harfler, hangi sırayla yazdırılır?

★★☆ safeParse(text, fallback) adında bir fonksiyon yaz. Metin geçerli JSON ise sonucunu, değilse fallback'i döndürsün ve hatayı console.warn ile kaydetsin.

★★☆ id property'si taşıyan bir NotFoundError class'ı yaz. findUser(users, id) kullanıcıyı bulamazsa bu hatayı fırlatsın. Çağıran tarafta NotFoundError'ı yakala, diğer bütün hataları yeniden fırlat.

★★★ Üç katmanlı bir zincir kur: readRaw bozuk bir JSON metni döndürsün, parseProfile onu çözmeye çalışıp hata alırsa cause ile sarsın, loadProfile da aynısını bir üst seviyede yapsın. Sonra printCauseChain(error) adında, while döngüsüyle cause'ları takip edip her mesajı bir öncekinden daha içeride yazdıran bir fonksiyon yaz. Hata kontrolü için yukarıdaki isError yardımcısını kullan.

Bu yazıda class ValidationError extends Error yazdık ama class, constructor, super ve extends'in ne olduğunu şimdilik kalıp olarak geçtik. Bir sonraki yazıda class'ları baştan ele alacağız: constructor, method'lar, getter ve setter, static üyeler, # ile private alanlar ve inheritance.