Ruby'de hata yönetimi, riskli kodu begin ile açılan bir bloğa koyup oluşan istisnayı rescue ile yakalamaya dayanır. else bölümü yalnızca hiç istisna çıkmadığında, ensure bölümü ise her durumda çalışır; retry bloğu baştan dener, raise yeni bir istisna fırlatır. Sınıf adı yazmadığınız bir rescue yalnızca StandardError ve onun alt sınıflarını yakalar.
Bu yazıda her parçayı Ruby 4.0 belgelerine dayanarak, çalıştırılmış örneklerle ele alıyoruz: birden fazla rescue yazarken sıranın etkisi, else ile ensure bölümlerinin dönüş değerine etkisi, kendi istisna sınıfınızı yazmak, Exception ile StandardError arasındaki farkın neden önemli olduğu ve metot gövdesinde begin yazmadan hata yakalamak. Dile yeni başlıyorsanız önce Ruby öğrenme rehberi sayfasına göz atabilirsiniz.
Temel yapı: begin, rescue, end

begin ile rescue arasındaki kod korunan bölgedir. Burada bir istisna oluştuğunda çalışma hemen kesilir ve uygun rescue bölümüne geçilir. rescue satırının sonuna => e yazarsanız yakalanan istisna nesnesi e değişkenine atanır; sınıfını e.class, mesajını e.message ile okursunuz. İstisna yakalandığı için program end satırından sonra kaldığı yerden devam eder.
begin
sonuc = 10 / 0
puts "Buraya gelinmez"
rescue ZeroDivisionError => e
puts "Yakalandı: #{e.class}"
puts "Mesaj: #{e.message}"
end
puts "Program devam ediyor"
# çıktı: Yakalandı: ZeroDivisionError
# çıktı: Mesaj: divided by 0
# çıktı: Program devam ediyorBelgelere göre rescue bölümünün içinde iki salt okunur genel değişken de dolar: $! yakalanan istisnayı, $@ ise onun backtrace dizisini tutar. Bu bölümün dışında ikisi de nil olur. Okunabilirlik için çoğu zaman => e ile isim vermek daha anlaşılırdır.
Birden fazla rescue ve sıranın önemi
Aynı blokta birden fazla rescue yazabilirsiniz. Ruby istisnayı yukarıdan aşağı doğru dener ve eşleşen ilk bölümü çalıştırır; geri kalanlar atlanır. Bir rescue satırı, yazdığınız sınıfı ve onun tüm alt sınıflarını yakalar. Aşağıdaki örnekte metot gövdesinin kendisi korunan bölge olarak kullanılıyor, bu yüzden ayrıca begin yazmaya gerek kalmıyor:
def donustur(deger)
Integer(deger)
rescue ArgumentError => e
"Geçersiz sayı: #{e.message}"
rescue TypeError
"Tür uygun değil"
end
p donustur("42")
p donustur("kırk iki")
p donustur(nil)
# çıktı: 42
# çıktı: "Geçersiz sayı: invalid value for Integer(): \"kırk iki\""
# çıktı: "Tür uygun değil"Tek satırda virgülle birden çok sınıf da verebilirsiniz. KeyError aslında IndexError sınıfının alt sınıfıdır; yine de ikisini birlikte yazmak niyetinizi açıkça gösterir. Bu örnekte rescue doğrudan bir do ... end bloğunun içinde duruyor; belgeler blokların da metotlar gibi begin olmadan istisna yakalayabildiğini belirtir:
ayarlar = {port: 8080}
liste = [1, 2, 3]
[-> { ayarlar.fetch(:host) }, -> { liste.fetch(10) }].each do |islem|
islem.call
rescue KeyError, IndexError => e
puts "#{e.class}: #{e.message}"
end
# çıktı: KeyError: key not found: :host
# çıktı: IndexError: index 10 outside of array bounds: -3...3else ve ensure bölümleri
else bölümü, korunan bölgede hiç istisna çıkmadıysa çalışır. ensure ise istisna çıksa da çıkmasa da, yakalansa da yakalanmasa da en sonda çalışır; dosya kapatmak, kilidi bırakmak veya geçici dosyayı silmek gibi temizlik işleri için doğru yer burasıdır. Dönüş değerine dikkat edin: else varsa ve istisna çıkmadıysa blok else bölümündeki son ifadeyi döndürür. ensure bölümü ise açık bir return yazmadığınız sürece dönüş değerini değiştirmez.
def dosya_isle(ad, hata: false)
puts "#{ad} açılıyor"
raise IOError, "#{ad} okunamadı" if hata
"işlendi"
rescue IOError => e
puts "Hata: #{e.message}"
"başarısız"
else
puts "Sorun çıkmadı"
"başarılı"
ensure
puts "#{ad} kapatıldı"
end
p dosya_isle("rapor.csv")
p dosya_isle("yedek.csv", hata: true)
# çıktı: rapor.csv açılıyor
# çıktı: Sorun çıkmadı
# çıktı: rapor.csv kapatıldı
# çıktı: "başarılı"
# çıktı: yedek.csv açılıyor
# çıktı: Hata: yedek.csv okunamadı
# çıktı: yedek.csv kapatıldı
# çıktı: "başarısız"İlk çağrıda metot "işlendi" değil "başarılı" döndürdü, çünkü istisna çıkmayınca else bölümü çalıştı ve son ifade oradan geldi. ensure içindeki puts her iki çağrıda da çalıştı ama dönüş değerine karışmadı.
retry ile yeniden denemek

retry yalnızca bir rescue bölümünün içinde geçerlidir; başka bir yerde kullanırsanız SyntaxError alırsınız. Çalıştığında korunan bölge en baştan yeniden çalışır, yani hatanın oluştuğu satırdan değil, begin satırından itibaren. Bu yüzden deneme sayacını bloğun dışında tutmanız ve bir üst sınır koymanız gerekir; aksi halde sonsuz döngü kurarsınız.
deneme = 0
begin
deneme += 1
puts "Deneme #{deneme}"
raise "Sunucu yanıt vermedi" if deneme < 3
puts "Bağlantı kuruldu"
rescue => e
puts "Hata: #{e.message}"
retry if deneme < 3
puts "Vazgeçildi"
end
# çıktı: Deneme 1
# çıktı: Hata: Sunucu yanıt vermedi
# çıktı: Deneme 2
# çıktı: Hata: Sunucu yanıt vermedi
# çıktı: Deneme 3
# çıktı: Bağlantı kurulduraise ile istisna fırlatmak
Kernel#raise birkaç biçimde çağrılır. Yalnızca bir metin verirseniz RuntimeError oluşur. Bir sınıf ve metin verirseniz o sınıftan bir istisna oluşur. Yalnızca sınıf verip metin vermezseniz mesaj, sınıfın adı olur. Belgelerdeki imza raise(exception, message = exception.to_s, backtrace = nil, cause: $!) biçimindedir.
def yas_kontrol(yas)
raise ArgumentError, "yaş negatif olamaz: #{yas}" if yas < 0
yas
end
begin
yas_kontrol(-5)
rescue => e
p e
end
begin
raise "Bir şeyler ters gitti"
rescue => e
p e.class
end
begin
raise ArgumentError
rescue => e
p e.message
end
# çıktı: #<ArgumentError: yaş negatif olamaz: -5>
# çıktı: RuntimeError
# çıktı: "ArgumentError"Yeniden fırlatma ve cause
Bir rescue bölümünde argümansız raise yazarsanız yakaladığınız istisna olduğu gibi tekrar fırlatılır. Bu, hatayı günlüğe yazıp üst katmana iletmek için kullanışlıdır. rescue içinde yeni bir istisna fırlattığınızda ise önceki istisna otomatik olarak yenisinin cause değerine yerleşir; böylece asıl nedeni kaybetmezsiniz.
def kaydet
1 / 0
rescue ZeroDivisionError => e
puts "Günlüğe yazıldı: #{e.message}"
raise
end
begin
kaydet
rescue => e
puts "Üst katmanda yakalandı: #{e.class}"
end
def ayar_oku(metin)
Integer(metin)
rescue ArgumentError
raise "Ayar dosyası bozuk"
end
begin
ayar_oku("abc")
rescue => e
puts e.message
puts e.cause.class
end
# çıktı: Günlüğe yazıldı: divided by 0
# çıktı: Üst katmanda yakalandı: ZeroDivisionError
# çıktı: Ayar dosyası bozuk
# çıktı: ArgumentErrorKendi istisna sınıfınızı yazmak
Belgeler, özel istisna sınıflarının yerleşik bir istisna sınıfından, çoğunlukla StandardError veya RuntimeError sınıfından türetilmesini önerir. Kendi sınıfınız hem hatayı türüne göre yakalamayı kolaylaştırır hem de istisnaya ek bilgi taşımanızı sağlar. Uygulamanız için bir üst sınıf tanımlayıp alt sınıfları ondan türetirseniz, çağıran kod isterse yalnızca üst sınıfı yakalayarak tüm aileyi kapsayabilir. Sınıf ve kalıtım konusunun ayrıntısı için Ruby class ve nesneler yazısına bakabilirsiniz.
class OdemeHatasi < StandardError; end
class YetersizBakiye < OdemeHatasi
attr_reader :eksik
def initialize(eksik)
@eksik = eksik
super("Bakiye yetersiz, #{eksik} TL eksik")
end
end
def ode(bakiye, tutar)
raise YetersizBakiye.new(tutar - bakiye) if tutar > bakiye
bakiye - tutar
end
begin
ode(100, 250)
rescue OdemeHatasi => e
puts e.message
puts e.eksik
p e.is_a?(StandardError)
end
# çıktı: Bakiye yetersiz, 150 TL eksik
# çıktı: 150
# çıktı: truesuper ile üst sınıfa verdiğiniz metin, istisnanın message değeri olur. Belgelere göre bir istisnanın mesajı oluşturulduktan sonra değiştirilemez; farklı mesajlı benzer bir nesne gerekirse Exception#exception metodunu yeni bir mesajla çağırabilirsiniz.
İstisna hiyerarşisi: StandardError ve Exception

Tüm istisnaların kökü Exception sınıfıdır, ancak uygulama kodunun karşılaştığı hataların büyük kısmı onun altındaki StandardError dalındadır. Sınıf adı yazmadan kullandığınız rescue tam olarak bu dalı yakalar. Exception sınıfının doğrudan altında ise program akışıyla ilgili özel sınıflar bulunur. Belgelerdeki hiyerarşiden seçilmiş bir özet:
| Sınıf | Üst sınıf | Ne zaman görülür |
|---|---|---|
| StandardError | Exception | Uygulama hatalarının ortak atası; çıplak rescue bunu yakalar |
| RuntimeError | StandardError | raise "mesaj" yazdığınızda oluşur |
| ArgumentError | StandardError | Yanlış sayıda ya da geçersiz argüman |
| TypeError | StandardError | Beklenmeyen nesne türü |
| NameError, NoMethodError | StandardError, NameError | Tanımsız ad veya metot |
| ZeroDivisionError | StandardError | Tamsayıyı sıfıra bölmek |
| KeyError | IndexError | Hash#fetch ile olmayan anahtar |
| FrozenError | RuntimeError | Dondurulmuş nesneyi değiştirmeye çalışmak |
| Errno::ENOENT ve diğer Errno | SystemCallError | İşletim sistemi çağrısı hataları |
| SyntaxError, LoadError | ScriptError | Kod ayrıştırılamadı veya dosya yüklenemedi |
| Interrupt | SignalException | Kullanıcı Ctrl+C ile programı kesti |
| SystemExit | Exception | exit çağrıldı |
| NoMemoryError, SystemStackError | Exception | Bellek tükendi, yığın taştı |
Bir sınıfın nereye oturduğunu kodla da görebilirsiniz. Aşağıdaki örnek ayrıca exit çağrısının çıplak rescue tarafından neden yakalanmadığını gösteriyor: SystemExit, StandardError dalında değildir.
p KeyError.ancestors.grep(Class).take_while { |k| k != Object }
p Interrupt.ancestors.include?(StandardError)
begin
exit(1)
rescue => e
puts "Bu satır çalışmaz"
rescue SystemExit => e
puts "SystemExit yakalandı, durum: #{e.status}"
end
# çıktı: [KeyError, IndexError, StandardError, Exception]
# çıktı: false
# çıktı: SystemExit yakalandı, durum: 1Metot, blok ve sınıf gövdesinde rescue
Yukarıdaki donustur ve dosya_isle örneklerinde gördüğünüz gibi, metot gövdesi kendiliğinden bir korunan bölgedir: def satırı begin görevini, metodun end satırı da bloğun sonunu üstlenir. Belgeler aynı kolaylığın class, module gövdeleri ve bloklar için de geçerli olduğunu söyler. Ayrı bir begin ... end yazmanız yalnızca korunan bölgeyi metodun bir kısmıyla sınırlamak istediğinizde gerekir. Metot tanımlamanın genel kuralları için Ruby metot tanımlama, bloklar için Ruby blok ve yield yazılarına bakabilirsiniz.
sonuclar = [0, 1, 2].map do |i|
10 / i
rescue ZeroDivisionError
nil
end
p sonuclar
# çıktı: [nil, 10, 5]Mesaj, backtrace ve Ruby 4.0 değişiklikleri
Her istisna nesnesi bir mesaj (message), isteğe bağlı bir backtrace (backtrace ve backtrace_locations) ve isteğe bağlı bir neden (cause) taşır. detailed_message mesaja sınıf adını ekler, full_message ise buna backtrace bilgisini de katarak terminalde gördüğünüz biçimi üretir. Günlük kaydı tutarken genellikle sınıf adı, mesaj ve backtrace satırlarını birlikte yazmak işinizi kolaylaştırır.
Ruby 4.0 NEWS dosyası bu alanda birkaç değişiklik listeler: ArgumentError oluştuğunda hata çıktısı artık hem çağıran satırı hem de metodun tanımını kod parçasıyla gösterir; "wrong number of arguments" hatasının backtrace satırı alıcı sınıfın adını içerir (bar yerine Foo#bar); backtrace içinde internal çerçeveler artık görünmez. Ayrıca Fiber#raise ve Thread#raise, Kernel#raise gibi cause: argümanını kabul eder hale geldi. Aşağıdaki çıktı Ruby 4.0.9 ile alınmıştır:
$ cat hata.rb
def topla(x, y) = x + y
topla(1)
$ ruby hata.rb
hata.rb:1:in 'Object#topla': wrong number of arguments (given 1, expected 2) (ArgumentError)
caller: hata.rb:3
| topla(1)
^^^^^
callee: hata.rb:1
| def topla(x, y) = x + y
^^^^^
from hata.rb:3:in '<main>'Ruby 4.0 ile gelen diğer yenilikleri toplu halde görmek için Ruby 4.0 yenilikler yazısına bakabilirsiniz.
Sık yapılan hatalar
- Her şeyi yakalamak:
rescue Exceptionprogram kapatma sinyallerini de yutar. VarsayılanStandardErrordavranışı neredeyse her zaman yeterlidir. - Hatayı sessizce yutmak:
rescueyazıp içini boş bırakmak, sorunun izini kaybettirir. En azından mesajı günlüğe yazın ya daraiseile yeniden fırlatın. - Çok geniş korunan bölge: Metodun tamamını tek bir
rescueile sarmak, beklemediğiniz satırlardaki hataları da aynı şekilde ele almanıza yol açar. Korunan bölgeyi gerçekten hata bekleyen satırlarla sınırlayın. - Sınırsız retry: Sayaç ve üst sınır olmadan
retrykullanmak, kalıcı bir hatada programı sonsuz döngüye sokar. - Her şeyi RuntimeError yapmak: Yalnızca
raise "mesaj"kullanırsanız çağıran kod hataları türüne göre ayıramaz. Anlamlı durumlar için kendi sınıfınızı tanımlayın. - ensure içinde değer döndürmeye çalışmak:
ensurebölümündeki son ifade dönüş değeri olmaz; temizlik dışında bir iş yüklemeyin.
Sık Sorulan Sorular
Sınıf adı yazmadığım rescue hangi hataları yakalar?
Yalnızca StandardError ve alt sınıflarını. ZeroDivisionError, ArgumentError, NoMethodError, KeyError gibi gündelik hataların hepsi bu daldadır. SystemExit, Interrupt veya SyntaxError bu daldan değildir ve çıplak rescue ile yakalanmaz.
ensure bölümü dönüş değerini etkiler mi?
Açık bir return yazmadığınız sürece hayır. Belgelere göre begin bloğu, ensure bölümüne girmeden önce değerlendirilen son ifadeyi döndürür. else bölümü varsa ve istisna çıkmadıysa bu, else içindeki son ifadedir.
Kendi istisna sınıfımı neden StandardError sınıfından türetmeliyim?
Çünkü çağıran kodun yazdığı çıplak rescue satırları yalnızca bu dalı yakalar. Sınıfınızı doğrudan Exception sınıfından türetirseniz, onu yakalamak isteyen herkes adını açıkça yazmak zorunda kalır ve beklenmedik yerlerde programın çökmesine yol açabilirsiniz.
Yakaladığım hatanın asıl nedenine nasıl ulaşırım?
rescue içinde yeni bir istisna fırlattığınızda önceki istisna yenisinin cause değerine kaydedilir. Üst katmanda e.cause ile zinciri geriye doğru izleyebilirsiniz; cause yoksa nil döner.
Dondurulmuş bir nesneyi değiştirmeye çalışınca hangi hata çıkar?
FrozenError oluşur; bu sınıf RuntimeError sınıfının alt sınıfıdır, dolayısıyla çıplak rescue ile yakalanır. Ayrıntılar için Ruby frozen string ve freeze yazısına bakın.
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: Ruby öğrenme rehberi · Ruby metot tanımlama · Ruby blok ve yield · Ruby class ve nesneler · Ruby pattern matching · Ruby frozen string · Ruby nedir