İçeriğe geç

Exception Yönetimi — Hata Yukarı Çıkarken Ne Kaybolur

Orta 8 dk Çok sık karşılaşılır

30 saniyede özet

Hatayı yakalamak kolay, doğru yakalamak zor. Boş bir catch hatayı sessizce yutar, sebebini eklemeden sarmak izi siler, finally içinde yanlış kapatılan bir kaynak da asıl hatanın üstünü örter.

Bir müşterinin siparişi ortadan kaybolmuş. Uygulama “tamam” demiş, log’da tek satır yok. Suçlu karmaşık bir hata değil, içi boş bir catch bloğu.

Aynı hata, üç yol: yutmak, izini koparmak, izini taşımak.
Adım adım oku
  1. Hata en alt katmanda, veritabanı çağrısında doğar: SQLException.
  2. Boş catch onu servis katmanında yutar. En üstte her şey tamam görünür, sipariş ise kaybolmuştur.
  3. Sebebini eklemeden yeni bir hataya sarmak, hatayı yukarı taşır ama asıl yerin izini koparır.
  4. Sebebiyle birlikte sarmak hem anlamlı bir hata verir hem de veritabanındaki asıl hataya giden izi korur.
  1. Bayt: Müşterinin siparişi kaybolmuş! Uygulama tamam demiş, log'da tek satır yok.

  2. Sen: Hata nereye gitti peki?

  3. Bayt: Birisi onu yakalayıp hiçbir şey yapmadan bırakmış: içi boş bir catch.

  4. Bayt: Hatayı yakaladıysan ya çöz ya da sebebiyle birlikte yukarı ilet. Paketi çöpe atmak yok!

Checked mi, unchecked mi

checked exceptionDerleyicinin yakalanmasını ya da imzada `throws` ile bildirilmesini zorunlu tuttuğu exception. `Exception`'dan türeyip `RuntimeException`'dan türemeyenler.Unchecked exception'lar (`RuntimeException` ve alt tipleri) bu zorunluluktan muaftır. Spring gibi çerçeveler checked exception'ları genelde unchecked olanlara çevirir.Sözlükte gör →, derleyicinin “bununla ilgilen” diye zorladığı hata türüdür: ya yakalarsın ya da throws yazıp işi çağırana bırakırsın. RuntimeException ve ondan türeyenler bu zorunluluktan muaftır.

TürÖrnekNe zaman
CheckedIOException, SQLExceptionÇağıranın toparlanabileceği dış dünya hatası
UncheckedIllegalArgumentException, NullPointerExceptionProgramlama hatası — yakalama, düzelt
ErrorOutOfMemoryError, StackOverflowErrorJVM’in kendi sorunu — yakalama

Çağıran bu hatayla anlamlı bir şey yapamıyorsa, onu checked yapma. Yoksa her çağıranı boş bir catch yazmaya zorlarsın. Spring de bu yüzden SQLException’ı unchecked DataAccessException’a çevirir.

Kafam karıştı, daha basit anlat

Checked hata, derleyicinin “bununla ilgilen” dediği hatadır. Dosya bulunamadı gibi, çağıranın bir şey yapabileceği durumlar için. Kodundaki bir yanlış ise unchecked olur: onu yakalamazsın, düzeltirsin.

Hızlı kontrolBaşlangıç

Checked ile unchecked exception arasındaki fark nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Her birini doğru gruba yerleştir.

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sınıflandırılmamış

Checked

Derleyici yakalamayı ya da throws'u zorlar

    Unchecked

    RuntimeException ve alt tipleri

      Error

      JVM'in kendi sorunu; normalde yakalanmaz

        Satır satır: iki hata, tek catch

        Peki bir iş patlarken, arkasından kapatmaya çalıştığın bağlantı da patlarsa? Ağ koptuğunda tam olarak bu yaşanır: iki hata üst üste.

        INSERT duplicate key ile patladı, ardından connection'ın close()'u da patladı. catch bloğuna hangi hata gelir? Cevabı göster

        Asıl hata: duplicate key. try-with-resources ikinci hatayı fırlatmaz, asıl hataya ekler. İkisi de log’a düşer.

        close() da patlarsa

        OrderRepository.java
        şu an çalışan satırtry (var conn = dataSource.getConnection()) {
        2 insert(conn, order);
        3} catch (SQLException e) {
        4 log.error("kayıt başarısız", e);
        5 var hidden = e.getSuppressed();
        6}

        Debug

        Adım 1/6

        repository Bağlantı alınır. try-with-resources, blok nasıl biterse bitsin close() çağıracağını garanti eder.

        conn
        = açık
        Java 21UTF-8LF1:1

        Sol/sağ ok tuşlarıyla da gezebilirsin.

        İkinci hataya suppressed exceptiontry-with-resources'ta kaynak kapatılırken atılan ve asıl hatanın yerine geçmek yerine ona eklenen ikinci exception. `getSuppressed()` ile okunur.Sözlükte gör → denir: asıl hatanın yanına not olarak iliştirilir. Elle yazılmış finally bunu yapmaz: oradan fırlayan hata asıl hatanın yerini alır ve asıl sebep iz bırakmadan kaybolur.

        Kafam karıştı, daha basit anlat

        Asıl hata ön sayfadır. Kapatırken çıkan ikinci hata, arkasına iliştirilmiş küçük bir nottur. try-with-resources ikisini de saklar, elle yazılan finally ise ön sayfayı kaybedebilir.

        Hızlı kontrolOrta

        Program ne yazdırır?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
        CloseOrder.java
        1record Res(String name) implements AutoCloseable {
        2 Res { System.out.println(name + " açıldı"); }
        3 public void close() { System.out.println(name + " kapandı"); }
        4}
        5
        6try (var a = new Res("A"); var b = new Res("B")) {
        7 System.out.println("gövde");
        8 throw new IllegalStateException();
        9} catch (IllegalStateException e) {
        10 System.out.println("catch");
        11}
        Java 21UTF-8LF

        Kendin gör

        Exception akışı — hata yukarı çıkarken ne kaybolur?

        Tohum 658401
        OrderFlow.java
        1// OrderRepository.insert
        2try (var conn = dataSource.getConnection()) {
        3 conn.prepareStatement(INSERT).executeUpdate(); // SQLException
        4} // close() burada, her durumda
        5
        6// OrderService.place
        7repository.insert(order); // yakalamıyor
        Java 21UTF-8LF
        1. OrderRepository.insertbekliyor
        2. Connectionbekliyor
        3. OrderService.placebekliyor
        4. İstemcibekliyor
        Hız
        Adım 0

        Şu an ne oldu?

        POST /orders geldi

        Aynı sipariş numarası ikinci kez yazılmaya çalışılacak. Hatayı katman katman izle.

        Görevler0/3

        • İstemci 200 alsın, sipariş kaydedilmesinaçık

          İpucu

          Service'in hatayı yakalayıp hiçbir şey yapmadığı seçeneği dene.

        • İstemci 500 alsın ama log'da SQLException hiç görünmesinaçık

          İpucu

          İki yolu var: cause vermeden sarmak ya da finally içindeki close() hatasının asıl hatayı ezmesi.

        • close() da patlarken iki hata da log'a düşsünaçık

          İpucu

          close() patlasın. Hangi kapatma biçimi ikinci hatayı atmak yerine asıl hataya ekler?

        Olay günlüğü (0)

        Henüz olay yok. Oynat veya adımla.

        1. Varsayılanla oynat. HTTP 500, log’da SQLException, bağlantı havuza döndü. Hedef bu.
        2. Service’i “yutuyor” yap. İstemci 200 aldı, log boş, sipariş yok.
        3. “Sarıyor, cause vermiyor” seç. 500 var ama log’da yalnızca OrderFailedException.
        4. close() patlasın, kapatmayı finally yap. Log bağlantıdan bahsediyor, duplicate key izi yok.
        5. “INSERT’ten sonra close()” seç. Hata görünüyor ama bağlantı sızdı.
        Hızlı kontrolOrta

        try-with-resources gövdesi SQLException fırlattı, ardından kaynağın close() metodu da bir exception fırlattı. catch bloğuna hangisi gelir?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Sararken sebebini de ekle

        Teknik bir hatayı (“veritabanı cevap vermedi”) iş diline (“sipariş alınamadı”) çevirmek doğru bir fikir. Ekrana cevap veren katmanın SQLException bilmesi gerekmez.

        OrderService.java
        try {
        repository.insert(order);
        } catch (SQLException e) {
        throw new OrderFailedException("sipariş kaydedilemedi", e); // e = cause
        }

        Çevirirken asıl sebebi (cause) de yanına koy. exception chainingBir exception'ı başka birine sararken asıl hatayı `cause` olarak geçirmek. Stack trace'teki "Caused by" satırları buradan gelir.Sözlükte gör → sayesinde log’da “Caused by: SQLException” satırı kalır. Sebebi eklemezsen iz, tam da hatanın çıktığı yerde kopar.

        • Yakaladığın yerde bir şey yap. Toparla ya da anlamını değiştir. İkisi de değilse yakalama.
        • Ya logla ya fırlat, ikisini birden değil. Her katman loglarsa bir hata beş alarm olur.
        • Kenarda tek yerde yakala. Spring’de bu @RestControllerAdvice’tır.
        Kafam karıştı, daha basit anlat

        Hatayı iş diline çevirebilirsin, ama asıl sebebi de yanına koy. Böylece log’u okuyan kişi, en alttaki gerçek sorunu hâlâ görebilir.

        Hızlı kontrolOrta

        Üretimde "sipariş kaydedilemedi" hatası görülüyor ama log'dan nedeni anlaşılamıyor. Hangi satırlar sorunun kaynağı?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Hatalı satıra dokun, sonra kontrol et.

        OrderService.java
        Java 21UTF-8LF

        Tuzaklar

        finally içinde return. Yolda olan hatayı sessizce yok eder ve metot hiçbir şey olmamış gibi “başarılı” döner.

        InterruptedException'ı yutmak· istersen atla

        Bu hatayı yakalıyorsan Thread.currentThread().interrupt() ile bayrağı geri koy. Yoksa thread’i durdurmak isteyen kod bu isteği bir daha göremez.

        Checked exception ve @Transactional. Spring varsayılan olarak yalnızca unchecked exception’da geri alır. Checked bir exception metottan çıkarsa transaction yine de kaydedilir; bunu istemiyorsan rollbackFor yaz.

        Hatayı normal akış için kullanmak. Exception üretmek ucuz değildir ve kodun niyetini gizler. “Bulunamadı” gibi zaten beklenen durumlar için Optional dön.

        Hızlı kontrolOrta

        `try { throw new IllegalStateException(); } finally { return 42; }` olan bir metot çağrıldığında ne olur?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Aşağıdaki örnek bir bankanın kredi taksidi tahsilatından: ana bankacılık sisteminin hatası sebebiyle birlikte sarılıyor, kaynaklar her yolda kapanıyor ve sınırda tek bir yerde anlamlı bir cevaba çevriliyor.

        Derinleş · Kredi taksidi tahsilatı: katmanlar boyunca exception 5 dosya · ~107 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/java/collection/ CollectionException.java Domain'in kendi hataları: sealed bir hiyerarşi, her biri ne olduğunu adıyla söyler.

        src/main/java/collection/CollectionException.java
        // abstract + sealed: a switch over it needs no default, the compiler knows every case.
        public abstract sealed class CollectionException extends RuntimeException
        permits CollectionException.InstallmentNotFound, CollectionException.CoreBankingUnavailable {
        CollectionException(String message, Throwable cause) {
        super(message, cause);
        }
        public static final class InstallmentNotFound extends CollectionException {
        public InstallmentNotFound(long installmentId) {
        super("installment " + installmentId + " not found", null);
        }
        }
        public static final class CoreBankingUnavailable extends CollectionException {
        public CoreBankingUnavailable(String system, Throwable cause) {
        super("core banking " + system + " unavailable", cause); // keep the cause
        }
        }
        }

        src/main/java/collection/ CoreBankingClient.java Altyapı katmanı: ana bankacılık sistemine giden çağrının checked IOException'ı, sebebi kaybetmeden domain hatasına sarılır.

        src/main/java/collection/CoreBankingClient.java
        public class CoreBankingClient {
        private final HttpClient http = HttpClient.newHttpClient();
        // Debits the customer's account for one loan installment.
        public String debit(long installmentId, String iban, BigDecimal amount) {
        var request = HttpRequest.newBuilder(URI.create("https://corebanking.internal/debits"))
        .header("Idempotency-Key", "installment-" + installmentId) // a retry never debits twice
        .POST(HttpRequest.BodyPublishers.ofString("{\"iban\":\"" + iban + "\",\"amount\":\"" + amount + "\"}"))
        .build();
        try {
        return http.send(request, HttpResponse.BodyHandlers.ofString()).body();
        } catch (IOException ex) {
        // Wrap, never swallow: the stack trace will show both layers.
        throw new CollectionException.CoreBankingUnavailable("corebanking.internal", ex);
        } catch (InterruptedException ex) {
        Thread.currentThread().interrupt(); // restore the flag before leaving
        throw new CollectionException.CoreBankingUnavailable("corebanking.internal", ex);
        }
        }
        }

        src/main/java/collection/ CollectionReport.java Kaynaklar try-with-resources ile kapanır; kapanırken çıkan hata kaybolmaz, suppressed olur.

        src/main/java/collection/CollectionReport.java
        // The end-of-day collection report for the loans department.
        public class CollectionReport {
        // Both resources are closed in reverse order on every path.
        // If writing fails AND closing fails, the close error is attached as "suppressed".
        public void export(Path target, List<String> lines) throws IOException {
        try (var out = Files.newBufferedWriter(target);
        var audit = new AuditLog(target.resolveSibling("collection-audit.log"))) { // AuditLog implements AutoCloseable
        for (String line : lines) {
        out.write(line);
        out.newLine();
        }
        audit.record("exported " + lines.size() + " lines");
        }
        }
        }

        src/main/java/collection/ Cli.java Sınır: exception'lar tek yerde çıkış koduna ve mesaja çevrilir, stack trace log'a gider.

        src/main/java/collection/Cli.java
        public class Cli {
        private static final System.Logger log = System.getLogger("collection");
        public static void main(String[] args) {
        int exitCode = run(args);
        System.exit(exitCode);
        }
        // The one place where exceptions become user-facing results.
        static int run(String[] args) {
        try {
        new CollectionService(new CoreBankingClient()).collect(Long.parseLong(args[0]));
        return 0;
        } catch (CollectionException ex) {
        log.log(System.Logger.Level.ERROR, "collection failed", ex); // full chain goes to the log
        System.err.println(switch (ex) {
        case CollectionException.InstallmentNotFound e -> "Taksit bulunamadı.";
        case CollectionException.CoreBankingUnavailable e -> "Ana bankacılık sistemine ulaşılamadı; tahsilat sonra tekrar denenecek.";
        });
        return 2;
        }
        }
        }

        src/test/java/collection/ ExceptionChainTest.java Test: sebep zinciri ve suppressed hatalar gerçekten taşınıyor mu?

        src/test/java/collection/ExceptionChainTest.java
        class ExceptionChainTest {
        @Test
        void providerFailureKeepsTheOriginalCause() {
        var io = new IOException("connection reset");
        var wrapped = new CollectionException.CoreBankingUnavailable("corebanking.internal", io);
        assertThat(wrapped).hasCause(io);
        assertThat(wrapped).hasRootCauseInstanceOf(IOException.class);
        }
        @Test
        void closeFailureIsSuppressedNotLost() {
        var writeError = new IllegalStateException("write failed");
        var closeError = new IllegalStateException("close failed");
        Throwable thrown = catchThrowable(() -> {
        try (AutoCloseable resource = () -> { throw closeError; }) {
        throw writeError;
        }
        });
        assertThat(thrown).isSameAs(writeError); // the primary error wins
        assertThat(thrown.getSuppressed()).containsExactly(closeError); // the other is kept
        }
        }

        Kendini sına

        Şimşek turu1/5

        İçi boş bir catch bloğu hatayı güvenle ortadan kaldırır.

        Soru 1/2İleri

        Bir worker döngüsünde `Thread.sleep()` çağrısının InterruptedException'ını yakalaman gerekiyor ve metot imzasına throws ekleyemiyorsun. Ne yaparsın?

        Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

        Aklında kalacak üç şey

        1. 1 Checked exception, çağıranın bir şey yapabileceği dış dünya hataları içindir: dosya yok, ağ koptu. Programcı hatası unchecked'tır.
        2. 2 try-with-resources, kapatırken çıkan hatayı asıl hatanın yerine koymaz, ona not olarak ekler. Elle yazılmış finally bunu yapmaz.
        3. 3 Bir exception'ı başka birine sararken sebebini (cause) eklemezsen, hatanın asıl çıktığı yerin izi kaybolur.
        Sonraki kapı Bir String listesi ile bir Integer listesi çalışırken aynı sınıftır. Tip bilgisi nereye kaybolur? Generics ve Type Erasure — Derleyicinin Bildiği, JVM'in Unuttuğu · 8 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

        Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.