← Kursa Dön
📄 Text · 12 min

Exception Best Practices

Exception handling bilmek bir şey, doğru yapmak başka bir şey. Yanlış kullanıldığında exception'lar hataları gizler, debug'ı imkansız kılar ve kodun güvenilirliğini yok eder. Bu derste "yapma" ve "yap" listesini net olarak çıkaracağız.

Exception handling bir yangın söndürme sistemi gibi. Kurmazsan bina yanar. Ama yanlış kurarsan — mesela alarmı kapatırsan — yangın olduğunu bile fark etmezsin. İkisi de felaket.

1. Spesifik Exception Yakala

En yaygın hata: her şeyi catch (Exception e) ile yakalamak. Bu, tüm hataları aynı kefeye koyar.

// KÖTÜ — çok genel
try {
    String input = getUserInput();
    int age = Integer.parseInt(input);
    User user = findUser(age);
} catch (Exception e) {
    System.out.println("Bir hata oluştu");
    // NumberFormatException mı? NullPointerException mı? 
    // UserNotFoundException mı? Hiçbir fikrin yok.
}

// İYİ — spesifik
try {
    String input = getUserInput();
    int age = Integer.parseInt(input);
    User user = findUser(age);
} catch (NumberFormatException e) {
    System.out.println("Geçersiz sayı: " + e.getMessage());
} catch (UserNotFoundException e) {
    System.out.println("Kullanıcı bulunamadı");
} catch (DatabaseException e) {
    logger.error("Veritabanı hatası", e);
    // Kullanıcıya genel mesaj göster
}

Spesifik yakalayınca:

  • Her hata için uygun tepki verebilirsin

  • Beklenmedik hatalar gizlenmez, yukarı fırlar

  • Debug kolay olur — hangi hata olduğunu bilirsin

2. Asla Throwable veya Error Yakalama

// ASLA YAPMA
try {
    doSomething();
} catch (Throwable t) {
    // OutOfMemoryError, StackOverflowError bile buraya düşer
    // Bu hatalara müdahale edemezsin!
}

// ASLA YAPMA
try {
    doSomething();
} catch (Error e) {
    // Aynı sorun
}

Error ve alt sınıfları JVM seviyesinde kurtarılamaz sorunlardır. Onları yakalasan bile:

  • OutOfMemoryError → catch bloğun bile çalışmayabilir

  • StackOverflowError → stack hâlâ dolu

  • Sistemi tanımsız durumda bırakırsın

Tek istisna: Framework yazıyorsan ve uygulamanın tamamını koruyan en üst seviye handler'daysan.

3. Boş catch Bloğu Yazma (Exception Swallowing)

Bu, exception handling'in en büyük günahı. Hatayı yakala, hiçbir şey yapma. Hata sessizce yutulur.

// KATİYEN YAPMA — "sessiz ölüm"
try {
    processPayment(order);
} catch (PaymentException e) {
    // Boş. Ödeme başarısız oldu ama kimse bilmiyor.
    // Kullanıcı ödendi sanıyor, sistem ödenmedi biliyor.
}

// KATİYEN YAPMA — yorum yazmak da yetmez
try {
    sendEmail(user);
} catch (MessagingException e) {
    // TODO: handle later  ← "later" asla gelmez
}

⚠️ Altın kural: Catch bloğu asla boş olmamalı. En azından logla. En azından neden ignore ettiğini açıkla.

// KABUL EDİLEBİLİR — bilinçli ignorance, belgelenmiş
try {
    Thread.sleep(100);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // Thread interrupt flag'ini koru
    // Bu pattern InterruptedException için standart
}

// KABUL EDİLEBİLİR — gerçekten önemsiz ve belgelenmiş
try {
    optionalCleanup();
} catch (CleanupException e) {
    logger.debug("Opsiyonel temizlik başarısız, devam ediliyor", e);
}

4. Exception'ı Logla, Düzgün Logla

Hata mesajını System.out.println ile yazdırmak production kodu için yeterli değil. Bir logging framework kullan (SLF4J + Logback, Log4j2).

// KÖTÜ
catch (Exception e) {
    System.out.println("Hata: " + e.getMessage());
    // Stack trace kayboldu, dosya/satır bilgisi yok
}

// KÖTÜ — sadece mesaj, stack trace yok
catch (Exception e) {
    logger.error("Hata: " + e.getMessage());
    // Stack trace olmadan hata bulmak çok zor
}

// İYİ — mesaj + tam exception (stack trace dahil)
catch (Exception e) {
    logger.error("Sipariş işlenemedi: orderId={}", orderId, e);
    // SLF4J son parametre Throwable ise otomatik stack trace basar
}
// Log seviyeleri doğru kullanılmalı
catch (UserNotFoundException e) {
    logger.warn("Kullanıcı bulunamadı: {}", userId);
    // warn — beklenebilir ama dikkat edilmeli
}

catch (DatabaseException e) {
    logger.error("Veritabanı bağlantı hatası", e);
    // error — ciddi sorun, müdahale gerekebilir
}

catch (ValidationException e) {
    logger.debug("Validasyon hatası: {}", e.getMessage());
    // debug — geliştirme aşamasında faydalı
}

5. Exception'ı Kontrol Akışı Olarak Kullanma

Exception'lar istisnai durumlar için. Normal program akışını yönetmek için değil.

// KÖTÜ — exception'ı if-else yerine kullanıyor
public boolean isInteger(String str) {
    try {
        Integer.parseInt(str);
        return true;
    } catch (NumberFormatException e) {
        return false;
    }
}

// DAHA İYİ — regex veya karakter kontrolü
public boolean isInteger(String str) {
    if (str == null || str.isEmpty()) return false;
    for (char c : str.toCharArray()) {
        if (!Character.isDigit(c) && c != '-') return false;
    }
    return true;
}
// KÖTÜ — koleksiyonda arama
try {
    while (true) {
        Object item = iterator.next(); // NoSuchElementException fırlar
        process(item);
    }
} catch (NoSuchElementException e) {
    // Döngü bitti
}

// İYİ — hasNext() kontrolü
while (iterator.hasNext()) {
    Object item = iterator.next();
    process(item);
}

Neden? Performans:

  • try bloğuna girmek ucuz ama exception fırlatmak pahalı (stack trace oluşturma)

  • Normal akışta exception fırlatırsan, gereksiz yere performans kaybedersin

  • Kod okunabilirliği düşer — okuyucu "bu niye try-catch içinde?" diye düşünür

6. catch-and-ignore Yerine catch-and-handle

Hatayı yakala ve bir şey yap. Sadece yukarıya fırlatmak veya sadece loglamak her zaman yeterli değil.

// ZAYIF — yakala, logla, kullanıcıyı bilgilendirme
catch (PaymentException e) {
    logger.error("Ödeme hatası", e);
    // Kullanıcı ne olduğunu bilmiyor!
}

// İYİ — yakala, logla, kullanıcıyı bilgilendir, alternatif sun
catch (PaymentException e) {
    logger.error("Ödeme hatası: userId={}, amount={}", userId, amount, e);
    notifyUser("Ödeme işlemi başarısız. Lütfen tekrar deneyin.");
    saveFailedPayment(userId, amount); // Tekrar deneme için kaydet
}

7. finally Yerine try-with-resources Kullan

// ESKİ ve ÇİRKİN
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
    conn = dataSource.getConnection();
    stmt = conn.prepareStatement("SELECT * FROM users");
    rs = stmt.executeQuery();
    // process...
} catch (SQLException e) {
    logger.error("SQL hatası", e);
} finally {
    if (rs != null) try { rs.close(); } catch (SQLException e) { }
    if (stmt != null) try { stmt.close(); } catch (SQLException e) { }
    if (conn != null) try { conn.close(); } catch (SQLException e) { }
}

// TEMİZ
try (
    Connection conn = dataSource.getConnection();
    PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users");
    ResultSet rs = stmt.executeQuery()
) {
    // process...
} catch (SQLException e) {
    logger.error("SQL hatası", e);
}

8. Exception Mesajları Bilgilendirici Olsun

// KÖTÜ — bilgi yok
throw new IllegalArgumentException("Geçersiz değer");

// KÖTÜ — hangi dosya? hangi satır? ne bekleniyordu?
throw new FileNotFoundException("Dosya bulunamadı");

// İYİ — bağlam bilgisi var
throw new IllegalArgumentException(
    "Yaş 0-150 arasında olmalı, verilen: " + age
);

// İYİ — debug için gerekli bilgiler
throw new FileNotFoundException(
    "Yapılandırma dosyası bulunamadı: " + configPath + 
    " (çalışma dizini: " + System.getProperty("user.dir") + ")"
);

İyi bir exception mesajı şunları içerir:

  • Ne oldu

  • Neden kabul edilmedi (beklenen vs gerçek)

  • Mümkünse çözüm önerisi

9. Exception Chaining'i Unutma

Alt seviye exception'ı dönüştürürken orijinalini her zaman cause olarak ekle.

// KÖTÜ — orijinal hata kayboldu
catch (SQLException e) {
    throw new ServiceException("İşlem başarısız");
    // SQLException'ın mesajı, stack trace'i — hepsi gitti
}

// İYİ — cause olarak korunuyor
catch (SQLException e) {
    throw new ServiceException("İşlem başarısız", e);
    // Log'da "Caused by: SQLException..." görürsün
}

10. return, break, continue finally İçinde Kullanma

// TEHLİKELİ
public int riskyMethod() {
    try {
        return 1;
        // veya: throw new RuntimeException();
    } finally {
        return 2; // try'daki return'ü ezer!
        // Exception da yutulur — asla fırlamaz!
    }
}

⚠️ finally içinde return yazmak exception'ları yutar! try bloğunda fırlayan exception, finally'deki return yüzünden kaybolur. Bu çok tehlikeli bir bug kaynağıdır.

11. Checked vs Unchecked Seçimi

// Kurtarılabilir, beklenen durum → Checked
public class InsufficientStockException extends Exception {
    // Stok yetersiz — çağıran bunu bilmeli ve handle etmeli
}

// Programcı hatası, bug → Unchecked
public class InvalidConfigurationException extends RuntimeException {
    // Yapılandırma yanlış — bu bir bug, fix edilmeli
}

Modern yaklaşım (Spring, Hibernate gibi framework'ler):

  • Çoğu durumda unchecked tercih et

  • Checked exception'lar API'yi kirletebilir — her method'a throws eklemek zorunda kalırsın

  • Checked kullan: çağıranın gerçekten bilmesi ve handle etmesi gerekiyorsa

Anti-Pattern Koleksiyonu

En sık görülen kötü pratikleri topladık:

// Anti-Pattern 1: Pokemon Exception Handling — "hepsini yakala!"
try {
    doEverything();
} catch (Exception e) {
    // "Gotta catch 'em all!"
    // Ne olduğunu bilmiyorsun, ne yapacağını da bilmiyorsun
}

// Anti-Pattern 2: Log and Throw — çift loglama
catch (IOException e) {
    logger.error("Hata", e);
    throw e; // Yukarıdaki catch de loglar → aynı hata iki kere loglanır
}
// Çözüm: Ya logla ya fırlat. İkisini birden yapma.
// İstisna: Farklı seviye/bağlam ekliyorsan kabul edilebilir.

// Anti-Pattern 3: Destructive Wrapping
catch (IOException e) {
    throw new ServiceException(e.getMessage()); // cause yok!
    // Doğrusu: new ServiceException(e.getMessage(), e)
}

// Anti-Pattern 4: catch içinde iş mantığı
try {
    user = findUser(id);
} catch (UserNotFoundException e) {
    user = createDefaultUser(id); // Exception'ı if-else gibi kullanıyor
}
// Doğrusu: Optional kullan veya findOrCreate pattern
// Anti-Pattern 5: throws Exception (çok genel bildirim)
public void doSomething() throws Exception { // Hangi exception?
    // Çağıranlar hepsini catch (Exception) ile yakalamak zorunda
}
// Doğrusu: Spesifik exception bildir
public void doSomething() throws IOException, ParseException { }

// Anti-Pattern 6: Yeni exception oluşturup stack trace kaybetme
catch (Exception e) {
    throw new RuntimeException("Hata oluştu");
    // Orijinal exception kayboldu!
}

İyi Pratik: Exception Handling Stratejisi

Bir projede tutarlı bir exception handling stratejisi belirle:

// 1. Proje bazlı base exception
public abstract class AppException extends RuntimeException {
    private final ErrorCode errorCode;
    
    protected AppException(ErrorCode code, String message) {
        super(message);
        this.errorCode = code;
    }
    
    protected AppException(ErrorCode code, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = code;
    }
    
    public ErrorCode getErrorCode() { return errorCode; }
}

// 2. Spesifik exception'lar
public class EntityNotFoundException extends AppException {
    public EntityNotFoundException(String entity, Object id) {
        super(ErrorCode.NOT_FOUND, entity + " bulunamadı: " + id);
    }
}

public class BusinessRuleException extends AppException {
    public BusinessRuleException(String rule) {
        super(ErrorCode.BUSINESS_RULE_VIOLATION, rule);
    }
}
// 3. Global exception handler (Spring örneği)
@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(EntityNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException e) {
        return ResponseEntity.status(404)
            .body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
    }
    
    @ExceptionHandler(BusinessRuleException.class)
    public ResponseEntity<ErrorResponse> handleBusiness(BusinessRuleException e) {
        return ResponseEntity.status(422)
            .body(new ErrorResponse(e.getErrorCode(), e.getMessage()));
    }
    
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception e) {
        logger.error("Beklenmedik hata", e);
        return ResponseEntity.status(500)
            .body(new ErrorResponse(ErrorCode.INTERNAL, "Sunucu hatası"));
    }
}

💡 İpucu: Global exception handler, tüm yakalanmamış exception'ları tek noktada yakalar. Her controller'da tekrar tekrar try-catch yazmaktan kurtarır.

Kontrol Listesi

Kod yazarken veya review yaparken bu listeyi kontrol et:

✅ Yap❌ Yapma
Spesifik exception yakalacatch (Exception e) her yerde
Exception'ı logla (stack trace ile)Boş catch bloğu
try-with-resources kullanfinally'de kaynak kapatma dansı
Bilgilendirici mesaj yazthrow new Exception("hata")
cause parametresini geçirOrijinal exception'ı kaybet
Ya logla ya fırlatİkisini birden yap
Unchecked tercih et (genelde)Her şeyi checked yap
Exception'ı handle etException'ı yut

Özet

  • Spesifik exception yakala — catch (Exception e) anti-pattern, her hata için uygun tepki ver

  • Boş catch bloğu en büyük günah — hatayı yutarsan bug'ları bulmak imkansızlaşır

  • Exception'ı kontrol akışı olarak kullanma — performans kaybı + okunabilirlik düşüşü

  • Loglama düzgün yap — stack trace dahil, doğru seviyede (warn/error), bağlam bilgisiyle

  • Exception chaining her zaman yap — new XException(message, cause) ile orijinal hatayı koru

  • Proje genelinde tutarlı exception stratejisi belirle — base exception, error codes, global handler