PHP'de bir işlemin yolunda gitmediğini bildirmenin modern yolu istisna (exception) fırlatmaktır. Sorunun fark edildiği yerde throw ile bir nesne atılır; bu nesneyi ele alabilecek kod try bloğuyla sarılır ve sınıfı eşleşen ilk catch bloğu nesneyi yakalar. finally bloğu ise sonuç ne olursa olsun çalışması gereken temizlik kodunu taşır. Fırlatılabilen her nesne Throwable arayüzünü uygular ve iki ana dala ayrılır: normal çalışma sırasında beklenebilecek durumlar için Exception, kodda düzeltilmesi gereken programlama hataları için Error.
Bu yazıda akışın adım adım nasıl ilerlediğini, Throwable hiyerarşisini, birden çok catch kullanımını, kendi istisna sınıfınızı yazmayı, finally ile return arasındaki şaşırtıcı etkileşimi ve yakalanmayan istisnaların nasıl göründüğünü PHP 8.5.12 ile çalıştırılmış örneklerle görüyorsunuz. Uyarı (Warning) ve bildirim (Notice) gibi geleneksel hata seviyeleri ayrı bir mekanizmadır; onlar için PHP error_reporting ve hata seviyeleri yazısına, belirli hata mesajlarının çözümü için TypeError ve ArgumentCountError rehberine bakabilirsiniz.
Temel akış: throw, try, catch ve finally
Aşağıdaki fonksiyon sıfıra bölmeye izin vermiyor ve geçersiz girdide InvalidArgumentException fırlatıyor. Çağıran taraf işlemi try içine alıyor, istisnayı yakalıyor ve her durumda bir mesaj yazdırıyor.
<?php
function bol(int $a, int $b): int
{
if ($b === 0) {
throw new InvalidArgumentException('Bölen sıfır olamaz');
}
return intdiv($a, $b);
}
try {
echo bol(10, 2), "\n"; // çıktı: 5
echo bol(5, 0), "\n"; // istisna burada fırlar
echo "Bu satır çalışmaz\n";
} catch (InvalidArgumentException $e) {
echo 'Hata: ', $e->getMessage(), "\n"; // çıktı: Hata: Bölen sıfır olamaz
} finally {
echo "finally her durumda çalışır\n";
}
echo "Program devam ediyor\n";Çıktı:
5
Hata: Bölen sıfır olamaz
finally her durumda çalışır
Program devam ediyorAkışı satır satır izlemek konuyu oturtur. İlk çağrı sorunsuz döner ve 5 yazılır. İkinci çağrıda throw çalıştığı anda fonksiyonun geri kalanı da, try bloğunun geri kalanı da atlanır; bu yüzden "Bu satır çalışmaz" metni hiç görünmez. PHP fırlatılan nesnenin sınıfına uyan ilk catch bloğunu arar, onu bulur ve içindeki kodu çalıştırır. Ardından finally çalışır ve program try yapısının hemen altından devam eder.
Kılavuza göre bir try bloğunun en az bir catch ya da bir finally bloğu olmak zorundadır. Eşleşen catch o fonksiyonda yoksa çağrı yığını (call stack) geriye doğru çözülür: istisna fonksiyonu çağıran koda, oradan da onu çağırana doğru ilerler. Yol üzerindeki tüm finally blokları çalıştırılır. En üst seviyeye kadar hiçbir yerde yakalanmazsa nesne set_exception_handler() ile kaydedilmiş bir işleyiciye verilir; işleyici yoksa betik ölümcül hata (fatal error) ile sonlanır.
throw bir ifadedir: ?? ve match ile kullanım
PHP 8.0'dan önce throw yalnızca tek başına bir deyim olarak yazılabiliyordu. 8.0 ile birlikte bir ifade (expression) oldu; artık değer beklenen her yerde, örneğin null birleştirme operatörü ??, üçlü operatör, match kolları ve arrow function gövdesinde kullanılabiliyor. Bu, "değer yoksa hata ver" kalıbını tek satıra indirir.
<?php
$ayar = ['ad' => 'keydal'];
$ad = $ayar['ad'] ?? throw new InvalidArgumentException('ad eksik');
echo $ad, "\n"; // çıktı: keydal
try {
$port = $ayar['port'] ?? throw new InvalidArgumentException('port eksik');
} catch (InvalidArgumentException) {
echo "port ayarı yok, varsayılan kullanılacak\n";
}Örnekteki ikinci catch bloğunda değişken adı yazılmadığına dikkat edin. PHP 8.0'dan beri yakalanan nesneye ihtiyacınız yoksa catch (InvalidArgumentException) biçiminde değişkeni atlayabilirsiniz. Blok yine çalışır, yalnızca nesneye erişemezsiniz. Kullanılmayan bir $e değişkeni tanımlamaktan daha açık bir niyet bildirir.
Throwable hiyerarşisi: Exception ile Error farkı

PHP 7'den beri motorun kendi ürettiği ciddi hataların çoğu da yakalanabilir nesnelerdir. Hepsinin ortak atası Throwable arayüzüdür; getMessage(), getCode(), getFile(), getLine(), getTrace(), getTraceAsString() ve getPrevious() metotlarını bu arayüz tanımlar. Kılavuz, kullanıcı sınıflarının Throwable arayüzünü doğrudan uygulayamayacağını açıkça belirtir: fırlattığınız nesne Exception ya da Error sınıfından veya bunların bir alt sınıfından türemelidir.
| Sınıf | Üst sınıf | Ne anlatır |
|---|---|---|
| Exception | Throwable | Normal çalışma sırasında ele alınabilecek durumlar |
| Error | Throwable | Kodda düzeltilmesi gereken programlama hataları |
| TypeError | Error | Parametre, dönüş veya özellik tipi uyuşmuyor |
| ArgumentCountError | TypeError | Kullanıcı fonksiyonuna eksik argüman verildi |
| ValueError | Error | Tip doğru ama değer geçersiz (ör. negatif tekrar sayısı) |
| ArithmeticError | Error | Aritmetik işlem hatası |
| DivisionByZeroError | ArithmeticError | Sıfıra bölme veya sıfıra göre mod alma |
| UnhandledMatchError | Error | match ifadesinde hiçbir kol eşleşmedi |
| ParseError | CompileError | Sözdizimi hatası |
| FiberError | Error | Fiber üzerinde geçersiz işlem |
Bu ayrımın pratik sonucu şudur: catch (Exception $e) yazdığınızda yalnızca Exception dalını yakalarsınız. Bir DivisionByZeroError ya da TypeError bu bloğun yanından geçip gider. Aşağıdaki örnek bunu gösteriyor:
<?php
try {
echo intdiv(10, 0);
} catch (Exception $e) {
echo "Exception bloğu\n";
} catch (DivisionByZeroError $e) {
echo get_class($e), ': ', $e->getMessage(), "\n";
}
try {
echo 10 % 0;
} catch (Throwable $t) {
echo get_class($t), ': ', $t->getMessage(), "\n";
}
var_dump(new DivisionByZeroError() instanceof Exception);
var_dump(new DivisionByZeroError() instanceof ArithmeticError);
var_dump(new DivisionByZeroError() instanceof Throwable);DivisionByZeroError: Division by zero
DivisionByZeroError: Modulo by zero
bool(false)
bool(true)
bool(true)İlk try yapısında catch (Exception $e) bloğu atlanır ve DivisionByZeroError için yazılan ikinci blok çalışır. intdiv() ile bölme ve % ile mod alma farklı mesajlar üretir: "Division by zero" ve "Modulo by zero". Son üç satır sınıf ilişkisini doğrular: bu hata bir Exception değildir ama ArithmeticError ve Throwable örneğidir. Kılavuz Error nesnelerini yakalamayı genel olarak önermez, çünkü bunlar bir programlama hatasına işaret eder. Uygulamanın en dış katmanında her şeyi kaydetmek için catch (Throwable $t) kullanmak ise yaygın ve makul bir kalıptır.
SPL istisnaları: hazır sınıfları kullanın

Her durum için yeni bir sınıf yazmadan önce Standart PHP Kütüphanesinin (SPL) hazır istisnalarına bakın. Bunlar iki gruba ayrılır. LogicException dalı, kılavuzun deyişiyle doğrudan kodun düzeltilmesini gerektiren mantık hatalarını anlatır. RuntimeException dalı ise ancak çalışma anında anlaşılabilen sorunlar içindir.
- LogicException altında:
InvalidArgumentException(argüman beklenen türde değil),DomainException(değer tanımlı geçerli alanın dışında),LengthException(uzunluk geçersiz),OutOfRangeException(geçersiz indeks istendi),BadFunctionCallExceptionve onun alt sınıfıBadMethodCallException. - RuntimeException altında:
OutOfBoundsException(değer geçerli bir anahtar değil),OverflowException(dolu bir kaba eleman eklemek),UnderflowException(boş kaptan eleman çıkarmak gibi),RangeException(çalışma anındaki aralık hataları) veUnexpectedValueException(dönen değer beklenen değer kümesinde değil).
Hazır sınıf kullanmanın faydası, kodunuzu okuyan kişinin istisnanın türünden ne olduğunu hemen anlamasıdır. Ayrıca hiyerarşi sayesinde çağıran taraf isterse tek tek sınıfları, isterse RuntimeException gibi üst sınıfı yakalayabilir. Veri yapıları tarafında SPL'nin SplStack veya SplFixedArray gibi sınıfları da bu istisnaları fırlatır; ayrıntılar için PHP SPL veri yapıları yazısına bakın.
Birden çok catch ve birleşik catch
Farklı hataları farklı şekilde ele almak için birden fazla catch bloğu art arda yazılır. PHP 7.1'den beri tek bir blokta birkaç sınıfı | işaretiyle birleştirebilirsiniz; farklı hiyerarşilerden gelen ama aynı şekilde ele alınacak istisnalar için idealdir. Aşağıdaki örnekte match kollarında throw kullanılıyor ve dördüncü girdi PHP'nin kendi fırlattığı bir ValueError üretiyor.
<?php
function oku(string $kaynak): string
{
return match ($kaynak) {
'yok' => throw new RuntimeException('Kaynak bulunamadı'),
'hatali' => throw new LengthException('Kaynak boş'),
'tekrar' => str_repeat($kaynak, -1),
default => "içerik: $kaynak",
};
}
foreach (['ayarlar', 'yok', 'hatali', 'tekrar'] as $k) {
try {
echo oku($k), "\n";
} catch (RuntimeException | LengthException $e) {
echo get_class($e), ' yakalandı: ', $e->getMessage(), "\n";
} catch (Error $e) {
echo 'Programlama hatası (', get_class($e), '): ', $e->getMessage(), "\n";
}
}içerik: ayarlar
RuntimeException yakalandı: Kaynak bulunamadı
LengthException yakalandı: Kaynak boş
Programlama hatası (ValueError): str_repeat(): Argument #2 ($times) must be greater than or equal to 0Blokların sırası önemlidir: PHP yukarıdan aşağıya bakar ve eşleşen ilk bloğu çalıştırır. Genel bir sınıfı (örneğin Exception) en üste yazarsanız altındaki daha özel bloklar hiçbir zaman çalışmaz. Kural basittir: en özel sınıf en üstte, en genel sınıf en altta. Bir başka ayrıntı: bir catch bloğunun içinden fırlatılan yeni istisna, aynı try yapısının yan yana duran öteki catch blokları tarafından yakalanmaz. Onu yakalamak için dışarıda ayrı bir try gerekir.
Kendi istisna sınıfınızı yazmak
Uygulamanıza özgü bir durumu anlatmak ve onu çağıran tarafta ayrı yakalayabilmek için Exception veya uygun bir SPL sınıfından türeyen kendi sınıfınızı yazabilirsiniz. Kurucu metodu yeniden tanımlıyorsanız kılavuz parent::__construct() çağırmanızı özellikle önerir; aksi halde mesaj, kod ve önceki istisna doğru atanmaz. Aşağıdaki sınıf ek bir siparisNo özelliği taşıyor ve alt katmandaki veritabanı hatasını "önceki istisna" olarak saklıyor.
<?php
class SiparisBulunamadi extends RuntimeException
{
public function __construct(
public readonly int $siparisNo,
?Throwable $onceki = null,
) {
parent::__construct("Sipariş bulunamadı: #$siparisNo", 404, $onceki);
}
}
function veritabanindanOku(int $no): array
{
throw new PDOException('no such table: siparisler');
}
function siparisGetir(int $no): array
{
try {
return veritabanindanOku($no);
} catch (PDOException $e) {
throw new SiparisBulunamadi($no, $e);
}
}
try {
siparisGetir(42);
} catch (SiparisBulunamadi $e) {
echo $e->getMessage(), "\n";
echo 'Kod: ', $e->getCode(), "\n";
echo 'Sipariş: ', $e->siparisNo, "\n";
echo 'Asıl neden: ', get_class($e->getPrevious()), ' - ', $e->getPrevious()->getMessage(), "\n";
}Sipariş bulunamadı: #42
Kod: 404
Sipariş: 42
Asıl neden: PDOException - no such table: siparislerBurada iki önemli kalıp var. Birincisi zincirleme (chaining): alt katman PDOException fırlattığında üst katman onu kendi anlamlı istisnasına sarar ve üçüncü argüman olarak $previous verir. Böylece çağıran taraf ayrıntıyı bilmek zorunda kalmaz ama gerektiğinde getPrevious() ile asıl nedene ulaşabilir. İkincisi ek veri: kurucuda tanıtılan public readonly özellik, hata mesajını ayrıştırmadan sipariş numarasına erişmeyi sağlar. Readonly özellikler hakkında ayrıntı içinpublic, private, protected ve readonly yazısına bakabilirsiniz.
Exception sınıfında getMessage(), getCode(), getTrace() gibi metotlar final olarak tanımlıdır, yani alt sınıfta değiştiremezsiniz. Metin gösterimini özelleştirmek isterseniz __toString() metodunu ezebilirsiniz. Ayrıca Throwable nesneleri kopyalanamaz: clone $e denemesi, alt sınıf kendi __clone() metodunu tanımlasa bile bir Error fırlatır.
finally ve return etkileşimi
finally bloğu, try ya da catch içinde return olsa bile çalışır. Kılavuzun anlattığı sıra şöyledir: return ifadesi karşılaşıldığı anda değerlendirilir, ama değer finally bloğu bittikten sonra döndürülür. finally içinde de bir return varsa döndürülen değer artık odur.
<?php
function deneme(): string
{
try {
return 'try';
} finally {
echo "finally çalıştı\n";
}
}
function ezen(): string
{
try {
throw new Exception('kayboldu');
} finally {
return 'finally kazandı';
}
}
echo deneme(), "\n";
echo ezen(), "\n";finally çalıştı
try
finally kazandıİkinci fonksiyon önemli bir tuzağı gösteriyor: try içinde fırlatılan istisna hiçbir yerde yakalanmadı, ama finally içindeki return onu sessizce yok etti. Bu yüzden finally bloğunda return kullanmaktan kaçının; bu blok dosya kapatmak, kilit bırakmak, geçici veriyi silmek gibi temizlik işleri içindir. Kılavuz iki ayrıntıyı daha belirtir: hem try hem finally istisna fırlatırsa finally bloğundaki istisna yayılır ve ilk istisna onun önceki istisnası olarak bağlanır. exit() çağrıldığında ya da betik ölümcül bir hatayla (örneğin azami çalışma süresi aşımıyla) aniden sonlandığında ise finally blokları çalışmaz.
Yakalanmayan istisnalar ve set_exception_handler
Hiçbir yerde yakalanmayan bir istisna betiği sonlandırır ve PHP mesajla birlikte yığın izini (stack trace) yazar. Aşağıdaki çıktıda dosya yolu kısaltılmıştır:
<?php
function yukle(string $dosya): void
{
throw new RuntimeException("$dosya açılamadı");
}
yukle('ayar.json');Fatal error: Uncaught RuntimeException: ayar.json açılamadı in /yol/ornek.php:4
Stack trace:
#0 /yol/ornek.php(7): yukle('ayar.json')
#1 {main}
thrown in /yol/ornek.php on line 4Mesaj üç bilgi verir: istisnanın sınıfı ve metni, fırlatıldığı dosya ile satır, ve oraya hangi çağrılarla gelindiği. #0 satırı en son çağrıyı, {main} ise betiğin en üst seviyesini gösterir. Bu tür mesajların okunması ve en sık görülen "Uncaught Error" türlerinin çözümü için PHP Fatal error: Uncaught Error çözümü yazısına bakın.
Uygulamanın tamamı için tek bir son savunma hattı kurmak isterseniz set_exception_handler() kullanılır. Kaydettiğiniz fonksiyon yakalanmayan her Throwable nesnesini alır. Kılavuz bunu tüm kodu bir try/catch içine almaya benzetir, tek farkla: işleyici döndükten sonra betik devam etmez, sonlanır.
<?php
set_exception_handler(function (Throwable $t): void {
echo 'Beklenmeyen hata: ', get_class($t), ' - ', $t->getMessage(), "\n";
});
echo "Başladı\n";
throw new LogicException('Yapılandırma okunamadı');
echo "Buraya hiç gelinmez\n";Başladı
Beklenmeyen hata: LogicException - Yapılandırma okunamadıÜretim ortamında bu işleyicinin tipik görevi, ayrıntıyı bir günlüğe yazmak ve kullanıcıya genel bir hata sayfası göstermektir; istisna mesajında veritabanı bilgisi gibi hassas veriler bulunabileceği için ayrıntıları ekrana basmamak gerekir. Hataların ekranda mı yoksa günlükte mi görüneceğini belirleyen display_errors ve log_errors sunucu ayarlarıdır; bu ayarlar için PHP hosting rehberi yazısına göz atabilirsiniz. PHP 8.5 ile o anda kayıtlı işleyiciyi döndüren get_exception_handler() fonksiyonu da eklendi.
Sık yapılan hatalar

- Uyarıları try/catch ile yakalamaya çalışmak.
echo $tanimsiz;gibi bir satır istisna değil uyarı (E_WARNING) üretir vecatchbloğuna hiç düşmez. Uyarıları istisnaya çevirmek içinset_error_handler()ileErrorExceptionfırlatmanız gerekir; nasıl yapılacağı hata seviyeleri yazısında anlatılıyor. - catch (Exception) ile her şeyi yakaladığını sanmak.
TypeError,ValueErrorveDivisionByZeroErrorgibi motor hatalarıErrordalındadır. Gerçekten her şeyi yakalamak istiyorsanızThrowableyazın. - Genel sınıfı en üste yazmak.
catch (Exception $e)ilk sıradaysa altındaki özel bloklar ölü koda dönüşür. - Boş catch bloğu. Hatayı yutmak sorunu ortadan kaldırmaz, yalnızca görünmez kılar. En azından günlüğe yazın ya da daha anlamlı bir istisnaya sarıp yeniden fırlatın.
- Önceki istisnayı kaybetmek. Bir istisnayı sararken
$previousargümanını vermezseniz asıl nedenin yığın izi kaybolur. - finally içinde return. Yukarıdaki örnekte görüldüğü gibi yakalanmamış istisnayı ortadan kaldırabilir.
- Kurucuda parent::__construct() çağırmamak. Mesaj ve kod boş kalır,
getPrevious()null döner.
Sık Sorulan Sorular
Exception mı yakalamalıyım, Error mı?
İş mantığınızın beklediği durumlar (dosya yok, kayıt bulunamadı, girdi geçersiz) için Exception ve alt sınıflarını kullanın. Error dalı kodda düzeltilmesi gereken hataları anlatır ve kılavuz bunları yakalamayı önermez. İstisna olarak, uygulamanın en dış katmanında her şeyi günlüğe yazmak için Throwable yakalamak makuldür.
Kendi istisnamı doğrudan Throwable arayüzünden türetebilir miyim?
Hayır. Kullanıcı sınıfları Throwable arayüzünü doğrudan uygulayamaz; Exception veya Error sınıfını (ya da bunların alt sınıflarını) genişletmelisiniz. Throwable uygulamayan bir nesneyi throw etmeye çalışmak da bir Error fırlatır.
finally bloğu ne zaman çalışmaz?
try veya catch içinde exit() çağrıldığında tüm finally blokları atlanır. Betiğin azami çalışma süresinin aşılması gibi ölümcül hatalarla aniden sonlanması da bu kuralın dışında kalır. Bunlar dışında, istisna yakalansa da yakalanmasa da finally çalışır.
catch bloğunda değişken yazmak zorunlu mu?
PHP 8.0'dan beri değil. catch (JsonException) gibi yalnızca sınıf adı yazabilirsiniz. Daha eski sürümleri desteklemeniz gerekiyorsa değişkeni yazmaya devam edin.
Hata mesajlarının metnine güvenerek karar vermeli miyim?
Mümkünse hayır. Mesaj metinleri sürümler arasında değişebilir; karar verirken istisnanın sınıfını ve gerekirse getCode() değerini kullanın. Örneğin JSON hatalarında kod, json_encode ve json_decode yazısında anlatılan JSON_ERROR_* sabitleriyle karşılaştırılabilir.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: PHP öğrenme rehberi · PHP hata seviyeleri · TypeError ve ValueError · Uncaught Error çözümü · match ifadesi