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ışmayabilirStackOverflowError→ stack hâlâ doluSistemi 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:
trybloğ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 yakala | catch (Exception e) her yerde |
| Exception'ı logla (stack trace ile) | Boş catch bloğu |
| try-with-resources kullan | finally'de kaynak kapatma dansı |
| Bilgilendirici mesaj yaz | throw new Exception("hata") |
| cause parametresini geçir | Orijinal exception'ı kaybet |
| Ya logla ya fırlat | İkisini birden yap |
| Unchecked tercih et (genelde) | Her şeyi checked yap |
| Exception'ı handle et | Exception'ı yut |
Özet
Spesifik exception yakala —
catch (Exception e)anti-pattern, her hata için uygun tepki verBoş 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ı koruProje genelinde tutarlı exception stratejisi belirle — base exception, error codes, global handler
AI Asistan
Sorularını yanıtlamaya hazır