Exception Handling Fundamentals
Bu modülde MiniBank API’sinde hata yönetimini standart hale getireceğiz. Amaç sadece exception yakalamak değil; API tüketicisine tutarlı, güvenli ve anlaşılır hata cevabı dönen bir error contract tasarlamak.
Hataları rastgele response’lara dönüşmeden, bilinçli bir API davranışına çevireceksin.
Hata akışını anlayacaksın
Controller içinde exception oluştuğunda Spring MVC’nin bunu nasıl yakaladığını ve hangi resolver zincirinden geçirdiğini göreceksin.
Global handler yazacaksın
@RestControllerAdvice ve @ExceptionHandler ile MiniBank için merkezi hata yönetimi oluşturacaksın.
Error contract tasarlayacaksın
Business hata, validation hata ve teknik hata için tutarlı JSON response yapısını MiniBank’a ekleyeceksin.
Önce kavramları sadeleştirelim.
Exception handling konusunu anlamak için önce “hata”, “exception”, “handler”, “contract” gibi kavramları ayırmak gerekir. Bu ayrım yapılmazsa her hata ya 500 döner ya da controller içinde dağınık try-catch blokları oluşur.
Exception
Java’da normal akışın devam edemediğini söyleyen nesnedir. Spring MVC’de controller veya service katmanından fırlayan exception, uygun handler bulunursa HTTP response’a çevrilir.
Business Exception
İş kuralı ihlalini temsil eder. Örneğin hesap bulunamadı, bakiye yetersiz veya müşteri zaten kayıtlı gibi hatalar business exception’dır.
Technical Exception
Sistemsel veya beklenmeyen teknik problemdir. Database bağlantı hatası, null pointer, timeout veya serialization hatası buna örnektir.
@RestControllerAdvice
Tüm REST controller’lar için ortak hata yakalama alanıdır. Controller’lara tek tek try-catch yazmak yerine merkezi davranış sağlar.
@ExceptionHandler
Belirli bir exception tipini hangi method’un handle edeceğini söyler. Örneğin AccountNotFoundException geldiğinde 404 döndürmek için kullanılır.
ResponseEntity
Body, status code ve header gibi HTTP response detaylarını kontrollü şekilde dönmemizi sağlar. Hata cevaplarında doğru status code vermek için sık kullanılır.
Validation Error
@Valid ile request DTO kontrol edilirken oluşan hatadır. Kullanıcıya hangi alanın neden hatalı olduğunu söylemek gerekir.
Error Contract
API’nin hata durumunda döneceği standart JSON yapısıdır. Frontend, mobil uygulama veya başka servisler bu contract’a güvenerek hata davranışı geliştirir.
Exception handling, hatayı saklamak değil; hatayı doğru sınıflandırıp doğru response’a çevirmektir.
REST API’de başarılı cevap kadar hatalı cevap da contract’ın parçasıdır. İyi tasarlanmış bir API, hata olduğunda bile tüketicisine ne olduğunu, hangi isteğin sorunlu olduğunu ve mümkünse nasıl düzeltebileceğini anlaşılır şekilde söyler.
Exception handling neden gerekli?
Bir controller method’u çalışırken üç farklı hata türüyle karşılaşabiliriz. Kullanıcı yanlış veri gönderebilir, iş kuralı izin vermeyebilir ya da sistem beklenmeyen teknik bir problem yaşayabilir. Bunların hepsini aynı şekilde ele alırsak API tüketicisi için karanlık bir kutu oluşur.
Validation hatası
Request formatı veya alanları hatalıdır. Örneğin amount negatif gönderilmiştir. Genellikle 400 Bad Request döner.
Business hata
Request formatı doğrudur ama iş kuralı izin vermez. Örneğin hesap yoktur veya bakiye yetersizdir. Duruma göre 404, 409 veya 422 dönebilir.
Technical hata
Beklenmeyen sistem hatasıdır. Kullanıcıya stack trace verilmez. Kontrollü, kısa ve güvenli bir 500 Internal Server Error cevabı döndürülür.
Exception handling’i “hata yakalama” olarak değil, exception → HTTP response dönüşüm katmanı olarak düşün. Service katmanı iş kuralı hatasını fırlatır; web katmanı bunu API contract’a uygun JSON response’a çevirir.
@PostMapping("/transfers")
public ResponseEntity<?> transfer(@Valid @RequestBody TransferRequest request) {
try {
transferService.transfer(request);
return ResponseEntity.status(HttpStatus.CREATED).build();
} catch (InsufficientBalanceException ex) {
return ResponseEntity.status(HttpStatus.CONFLICT)
.body(Map.of("message", "Insufficient balance"));
} catch (AccountNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", "Account not found"));
}
}
Bu kod çalışır ama büyüdükçe problem çıkarır. Her controller aynı try/catch bloklarını tekrarlar, hata response formatı controller’dan controller’a değişebilir ve teknik detayların dışarı sızma riski artar. Daha temiz olan yaklaşım, controller’ı sade tutup hata dönüşümünü merkezi hale getirmektir.
Spring MVC exception akışı nasıl işler?
Controller method’u exception fırlattığında Spring MVC request’i hemen kaybetmez. DispatcherServlet, oluşan exception’ı çözebilecek bir handler arar. Bu noktada devreye exception resolver zinciri girer.
@ExceptionHandler veya default resolver response üretir.Bu mekanizmanın önemli tarafı şudur: Hata oluştu diye response mutlaka 500 olmak zorunda değildir. Sen doğru exception tipini doğru handler ile eşlersen, iş kuralı hatalarını anlamlı status code ve body ile döndürebilirsin.
Hata yönetimi sadece “hangi exception hangi status code?” meselesi değildir. Aynı zamanda güvenlik meselesidir. Stack trace, class adı, SQL detayı, internal servis URL’i veya exception message içindeki hassas veri dışarı sızmamalıdır.
@RestControllerAdvice ve @ExceptionHandler
@RestControllerAdvice, REST controller’lar için global bir hata yakalama katmanı sağlar. Burada yazdığımız @ExceptionHandler method’ları, uygulamanın farklı controller’larından gelen exception’ları tek merkezde karşılar.
Bu yaklaşım controller’ları sadeleştirir. Controller sadece request’i alır, service’i çağırır ve başarılı response’u döner. İş kuralı ihlalinde service exception fırlatır; global handler bunu API cevabına çevirir.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccountNotFoundException.class)
public ResponseEntity<ApiErrorResponse> handleAccountNotFound(
AccountNotFoundException ex,
HttpServletRequest request
) {
ApiErrorResponse response = ApiErrorResponse.of(
"ACCOUNT_NOT_FOUND",
ex.getMessage(),
request.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
}
Burada dikkat edilmesi gereken şey, handler method’unun sadece response üretmesidir. Business karar burada verilmez. “Hesap gerçekten var mı?” kararı service katmanındadır. Handler sadece “bu exception API’de nasıl temsil edilecek?” sorusuna cevap verir.
Business exception ile technical exception ayrımı
Business exception, beklenen ve domain tarafından anlamı olan hatadır. Kullanıcı hatalı bir işlem yapmış olabilir veya iş kuralı bu işleme izin vermiyor olabilir. Technical exception ise çoğu zaman beklenmeyen ve kullanıcıya detaylı açıklanmaması gereken sistem hatasıdır.
Business exception
- Domain dilinde anlamlıdır.
- API tüketicisine kontrollü mesaj dönebilir.
- Status code iş kuralına göre seçilir.
- Örnek:
InsufficientBalanceException
Technical exception
- Sistemsel veya beklenmeyen problemdir.
- Detayı dışarı verilmez.
- Loglanır, kullanıcıya güvenli mesaj döner.
- Örnek:
DataAccessException
MiniBank için örnek hata kodları
İstenen hesap bulunamadı.
Transfer için bakiye yeterli değil.
Request alanları geçersiz.
Beklenmeyen teknik hata.
Validation hatalarını özel ele almak
Bir önceki modülde request DTO ve validation ekledik. Şimdi bu validation hatalarının dışarı nasıl döneceğini standart hale getireceğiz. Çünkü validation hatasında sadece “Bad Request” demek çoğu zaman yetmez; hangi alanın neden hatalı olduğunu da söylemek gerekir.
{
"code": "VALIDATION_ERROR",
"message": "Request validation failed",
"path": "/api/v1/accounts",
"fieldErrors": [
{
"field": "iban",
"message": "IBAN boş olamaz"
},
{
"field": "balance",
"message": "Bakiye negatif olamaz"
}
]
}
Bu format frontend ve mobil ekip için çok değerlidir. Çünkü kullanıcıya alan bazlı mesaj gösterebilirler. API tüketicisi sadece 400 status code’a bakmak zorunda kalmaz; response body üzerinden hangi input’u düzeltmesi gerektiğini bilir.
Standard error response modeli
Error contract tasarlarken temel amaç tutarlılıktır. Aynı uygulamada bazı hataların message, bazılarının error, bazılarının detail alanıyla dönmesi API tüketicisini yorar. MiniBank’ta tek bir standart response modeli kullanacağız.
public record ApiErrorResponse(
String code,
String message,
String path,
Instant timestamp,
List<FieldErrorResponse> fieldErrors
) {
public static ApiErrorResponse of(String code, String message, String path) {
return new ApiErrorResponse(
code,
message,
path,
Instant.now(),
List.of()
);
}
public static ApiErrorResponse validation(
String message,
String path,
List<FieldErrorResponse> fieldErrors
) {
return new ApiErrorResponse(
"VALIDATION_ERROR",
message,
path,
Instant.now(),
fieldErrors
);
}
}
Bu eğitimde anlaşılır olması için custom ApiErrorResponse modeli kullanacağız. Spring Boot 3 projelerinde ProblemDetail yaklaşımı da değerlendirilebilir; ancak eğitimde önce error contract mantığını net oturtmak daha değerli.
MiniBank Case 06 — Global Exception Handling & Standard Error Contract
Bu case’de MiniBank API’si için merkezi hata yönetimi ekleyeceğiz. Controller içinde dağınık try/catch yazmayacağız; business exception’ları ve validation hatalarını tek bir global handler üzerinden standart response’a çevireceğiz. Görev listesi, doğrulama komutları, kod adımları ve kabul kriterleri Lab Dashboard'da.
Lab sırasında geride kalırsan lab-06-complete branch'ine geçip herkesle aynı checkpoint'ten devam edebilirsin.
Bu modülde akılda kalması gerekenler
Başarılı response kadar hatalı response da tutarlı ve tahmin edilebilir olmalıdır.
Merkezi hata yönetimi controller’ları sade, response formatını tutarlı tutar.
İş kuralı hatası kullanıcıya anlamlı dönebilir; teknik hata güvenli ve sınırlı bilgiyle dönmelidir.
Frontend ve mobil ekipler hangi alanın neden hatalı olduğunu response üzerinden okuyabilmelidir.
Servis tüketicileri her endpoint’te aynı hata formatını bekleyebilirse entegrasyon daha güvenli olur.
5 kısa kontrol sorusu
1. @RestControllerAdvice ne işe yarar?
Tüm REST controller’lar için global hata yakalama ve response üretme mekanizması sağlar.
2. Business exception ile technical exception arasındaki temel fark nedir?
Business exception iş kuralı ihlalidir ve kullanıcıya kontrollü mesaj dönebilir. Technical exception sistemsel problemdir ve detayı dışarı sızdırılmamalıdır.
3. Validation hatalarında neden field bazlı response önemlidir?
API tüketicisi hangi alanın neden hatalı olduğunu anlayabilir ve kullanıcı arayüzünde doğru mesajı gösterebilir.
4. Controller içinde try/catch yazmak neden uzun vadede problem olabilir?
Tekrar üretir, response formatını tutarsızlaştırır ve controller’ın sorumluluğunu artırır.
5. Standart error contract neden gereklidir?
Frontend, mobil ve servis entegrasyonlarının hata response’larını tahmin edilebilir şekilde işlemesini sağlar.