Tüm Rust rehberleri
TEKNİK REHBER HATA YÖNETİMİ RESULT 2026

Hata Yönetimi
Exception yok, panik son çare

Option ve Result tipleriyle null'sız, exception'sız hata akışı; ? operatörü, panic! ve hata dönüşümü — C errno ve C++ exception'larıyla karşılaştırmalı.

00 Exception'sız dünya

C'de hata bir dönüş kodu ya da global errno; gözden kaçırması serbest. C++'ta exception gizli bir çıkış kapısı. Rust hatayı tip sistemine taşır: hata bir değerdir, derleyici onu yok saymana izin vermez.

C'nin sessiz tuzağı

C'de hatayı kontrol etmek bir nezaket meselesidir. Dönüş kodunu okumayı unutursan derleyici tek kelime etmez:

read.c
// fopen NULL dönebilir; kontrol etmeyi unutmak derlenir, çalışır, çöker.
FILE *f = fopen("yok.txt", "r");
fread(buf, 1, 64, f);   // f == NULL → segfault, errno hiç okunmadı

errno bir thread-local global; çağrı ile kontrol arasına giren herhangi bir libc fonksiyonu onu ezebilir. Kontrol akışı ile veri akışı birbirinden ayrı, bu yüzden senkronize tutmak programcının sorumluluğunda.

C++ exception: görünmez çıkış

Exception'lar kontrol akışını kod metninde görünmez kılar. foo() çağrısının fonksiyondan fırlayıp çıkabileceğini imzasından anlayamazsın (noexcept dışında imza sessizdir). Bu, "exception-safe" kod yazmayı ve doğru noktalarda yakalamayı zorlaştırır; üstelik fırlatma anında stack unwinding maliyeti gizlidir.

Rust'ın ayrımı: recoverable vs unrecoverable

Rust hatayı ikiye böler. Recoverable (kurtarılabilir) hatalar Result<T, E> ile tipte taşınır — dosya yok, parse başarısız. Unrecoverable (kurtarılamaz) durumlar panic! ile programı sonlandırır — invariant ihlali, dizilim sınırı aşımı. Exception yoktur; hata akışı sıradan değer akışıdır.

C errno / dönüş kodu   →  kontrol opsiyonel, gözden kaçar
C++ exception          →  gizli kontrol akışı, görünmez çıkış
Rust Result / Option   →  hata = değer, derleyici zorlar
NOT

Rust'ta "hata fırlatmak" diye bir şey yoktur. Hata, fonksiyonun normal dönüş değerinin parçasıdır. Bu yüzden imzaya bakarak fonksiyonun hata üretip üretmediğini her zaman görürsün.

Bu bölümde

  • C'de dönüş kodu/errno kontrolü opsiyonel ve gözden kaçmaya açık
  • C++ exception kontrol akışını imzada görünmez kılar, unwinding maliyeti gizli
  • Rust hatayı tip sistemine taşır: hata bir değerdir, yok sayılamaz
  • recoverable → Result/Option, unrecoverable → panic! ayrımı

01 Option<T> — null'a veda

C'de "değer yok" durumu null pointer veya sihirli bir değerle (-1, NULL) anlatılır ve kontrolü unutursun. Rust'ta yokluk tip seviyesinde: Option<T> ya Some(v) ya da None'dur, ve T'ye dokunmadan önce hangisi olduğunu çözmen gerekir.

Tanım

Option standart kütüphanede sıradan bir enum'dur — dil sihri değil:

option.rs
enum Option<T> {
    None,        // değer yok — C'deki NULL'ın güvenli karşılığı
    Some(T),     // değer var, içinde T taşıyor
}

Kritik fark: Option<&str> ile &str farklı tiplerdir. Bir Option'ı doğrudan referans gibi kullanamazsın; önce Some/None ayrımını yapmak zorundasın. Null deref derleme zamanında imkânsız hale gelir.

std'de Option dönen tipik fonksiyonlar

main.rs
let v = mut; // (örnek için) vec![10, 20, 30]
let v = vec![10, 20, 30];

// get: indeks taşarsa None döner — C'de v[99] UB iken burada güvenli
let a: Option<&i32> = v.get(1);    // Some(&20)
let b: Option<&i32> = v.get(99);   // None

// iterator find: koşulu sağlayan ilk eleman ya da None
let c = v.iter().find(|&&x| x > 15);  // Some(&20)

// HashMap::get da Option döner
use std::collections::HashMap;
let mut m = HashMap::new();
m.insert("k", 1);
let d = m.get("yok");             // None — anahtar yoksa çökmez
NOT

Option<Box<T>> ve benzeri "nullable pointer" durumlarında Rust niche optimization yapar: None sıfır pointer ile temsil edilir, dolayısıyla bellek maliyeti çıplak pointer ile aynıdır. Güvenlik bedava gelir.

Bu bölümde

  • Option<T> = Some(v) | None; "değer yok" durumunu tip seviyesinde kodlar
  • Null deref derleme zamanında imkânsız — Option ile T farklı tiplerdir
  • get, find, HashMap::get gibi std fonksiyonları Option döndürür
  • Niche optimization sayesinde Option<Box<T>> çıplak pointer kadar ucuz

02 Result<T, E>

Option "var mı yok mu" der ama neden yok bilgisini taşımaz. Hatanın sebebi önemliyse Result<T, E> kullanılır: başarıda Ok(T), başarısızlıkta hata bilgisini taşıyan Err(E). Hata, dönüş tipinin bir parçasıdır.

Tanım ve #[must_use]

result.rs
// std'deki tanım; #[must_use] sonucu yok saymayı uyarıya çevirir.
#[must_use]
enum Result<T, E> {
    Ok(T),     // başarı: T değeri
    Err(E),    // hata: E tipinde sebep
}

C'de bir fonksiyonun dönüş kodunu kullanmadan bırakmak sessizdir. Rust'ta Result #[must_use] ile işaretlidir: sonucu hiç kullanmadan atarsan derleyici uyarır.

main.rs
use std::fs::File;

fn main() {
    File::create("log.txt");   // warning: unused `Result` that must be used
}
DİKKAT

#[must_use] yalnızca uyarıdır, hata değil. let _ = ... ile bilinçli olarak susturulabilir; ama bunu yaparken hatayı gerçekten görmezden gelmeyi kabul ettiğini belgelemiş olursun. C'deki sessiz yok saymadan farkı: artık niyet açık.

Sebep taşıyan E tipi

E herhangi bir tip olabilir: bir String, bir enum, std::io::Error ya da kendi tanımladığın hata tipi. C++ exception hiyerarşisinin aksine E tek bir somut tiptir ve imzada açıkça görünür:

Ok(T)İşlem başarılı, T tipinde sonuç taşır
Err(E)İşlem başarısız, E tipinde hata sebebi taşır
Result<(), E>Sonuç değeri yok, sadece başarı/başarısızlık (yan etki fonksiyonları)

Bu bölümde

  • Result<T, E> = Ok(T) | Err(E); hatanın sebebini de tipte taşır
  • #[must_use] sonucu yok saymayı derleyici uyarısına çevirir
  • E somut, imzada görünen tek bir tiptir — exception hiyerarşisi yok
  • Result<(), E> sadece başarı/başarısızlık bildiren işlemler için

03 match ile ele alma

Option ya da Result içindeki değere ulaşmanın temel yolu match'tir. match exhaustive'dir: her olası varyantı ele almak zorundasın, aksi halde derlenmez. Bu, C'de bir switch'te default'u veya bir case'i unutmanın karşılığını imkânsız kılar.

Result'ı açmak

main.rs
use std::fs;

fn oku(yol: &str) {
    match fs::read_to_string(yol) {
        Ok(icerik) => println!("{} bayt okundu", icerik.len()),
        Err(e)      => eprintln!("okunamadı: {e}"),
    }
    // İki kolu da yazmazsan: error[E0004] non-exhaustive patterns
}

Derleyici her iki kolu da ele almaya zorlar. Err kolunu silersen kod derlenmez. C'de hatayı kontrol etmemek serbestti; burada unutmak imkânsız.

Option'ı açmak ve içe bakış

main.rs
fn ilk_kelime(s: &str) -> &str {
    match s.split_whitespace().next() {
        Some(k) => k,
        None    => "",   // boş girdi durumu açıkça ele alındı
    }
}

// if let: yalnızca tek kola ilgileniyorsan match'in kısası
if let Some(k) = "merhaba dünya".split_whitespace().next() {
    println!("ilk: {k}");
}
NOT

Match desen eşleştirmesiyle değeri aynı anda bağlar: Ok(icerik) kolu hem Ok olduğunu doğrular hem içeriği icerik değişkenine açar. C++'ta if (result.has_value()) { result.value(); } iki ayrı adımken Rust'ta tek deyimdir, ve None dalında değere erişmek mümkün değildir.

Bu bölümde

  • match exhaustive: tüm varyantları ele almazsan derlenmez
  • Ok/Err ve Some/None kollarının ikisini de yazmak zorunludur
  • Desen eşleştirme değeri doğrular ve aynı anda bağlar
  • Tek kola ilgileniyorsan if let, match'in kısa biçimidir

04 ? operatörü

Her çağrıda match yazmak boilerplate üretir. ? operatörü bunu tek karaktere indirir: Ok ise içindeki değeri çıkarır, Err ise hatayı erken dönüşle çağırana yayar. C'deki "kontrol et, hatalıysa return" kalıbının dildeki karşılığıdır.

match boilerplate'inden ?'a

main.rs
use std::fs::File;
use std::io::{self, Read};

// Önce: elle match — gürültülü
fn oku_uzun(yol: &str) -> Result<String, io::Error> {
    let mut f = match File::open(yol) {
        Ok(f)  => f,
        Err(e) => return Err(e),
    };
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_)  => Ok(s),
        Err(e) => Err(e),
    }
}

// Sonra: ? ile aynı mantık, dört satır
fn oku_kisa(yol: &str) -> Result<String, io::Error> {
    let mut f = File::open(yol)?;       // Err ise hemen return Err(...)
    let mut s = String::new();
    f.read_to_string(&mut s)?;            // Err ise hemen return Err(...)
    Ok(s)
}

? için fonksiyon Result/Option dönmeli

? erken dönüş yaptığı için, kullanıldığı fonksiyonun dönüş tipi uyumlu olmalıdır. Result içinde ? kullanan fonksiyon Result dönmeli; Option içinde ? kullanan fonksiyon Option dönmeli.

main.rs
// Option üzerinde ?: None ise erken None döner
fn ilk_harf(s: &str) -> Option<char> {
    let kelime = s.split_whitespace().next()?;  // None → return None
    kelime.chars().next()                       // Option<char>
}
DİKKAT

?'u fn main() içinde kullanmak istiyorsan main'in dönüş tipini Result<(), E> yapmalısın. Aksi halde "the ? operator can only be used in a function that returns Result or Option" hatası alırsın. main için tipik desen: fn main() -> Result<(), Box<dyn Error>>.

Bu bölümde

  • ? operatörü Ok/Some değerini çıkarır, hata durumunda erken döner
  • match boilerplate'ini tek karaktere indirir, hata yayma kalıbını dile gömer
  • Result içinde Result, Option içinde Option döndüren fonksiyon gerektirir
  • main'de ? kullanmak için dönüş tipi Result<(), E> olmalı

05 unwrap / expect

unwrap ve expect kestirme yoldur: Ok/Some ise değeri verir, değilse panic! eder. Hızlıdır ama hata akışını kısa devre yapar. Üretim kütüphanesinde nadir, prototip ve testte yaygındır.

İkisi arasındaki fark

main.rs
let n: i32 = "42".parse().unwrap();
// parse başarısızsa: panicked at 'called `Result::unwrap()` on an `Err` value: ...'

let n: i32 = "42".parse()
    .expect("yapılandırma sayısı geçerli olmalı");
// expect aynı işi yapar ama panik mesajı senin bağlamını taşır

expect daima unwrap'a tercih edilir: panik mesajı "neden burada bunun başarılı olmasını bekliyordum" sorusunu yanıtlamalı. İyi bir expect mesajı, koşulun neden tutması gerektiğini anlatır — hatanın kendisini değil.

Ne zaman kabul edilebilir?

Durumunwrap/expect?
Prototip / keşifKabul edilebilir, hızlı ilerleme
Test koduKabul edilebilir, panik = test başarısız
Gerçekten imkânsız durumexpect + gerekçe yorumu ile
Kütüphane API'siKaçın — Result döndür, kararı çağırana bırak
Kullanıcı girdisine bağlıAsla — saldırı/çökme yüzeyi
main.rs
// "Gerçekten imkânsız" durumda gerekçeli expect:
let re = Regex::new(r"^\d+$")
    .expect("sabit literal regex; derleme zamanı doğru");

// unwrap_or: panik yerine varsayılan — çoğu zaman daha iyi seçim
let port: u16 = std::env::var("PORT")
    .ok()
    .and_then(|s| s.parse().ok())
    .unwrap_or(8080);
DİKKAT

unwrap, hatayı kontrol etmeyi unuttuğun C koduna benzer bir çökme riski getirir — ama burada bilinçli ve görünür. Yine de production'da çağrılan her unwrap potansiyel bir panik noktasıdır. clippy::unwrap_used lint'i ile bunları derlemede yasaklayabilirsin.

Bu bölümde

  • unwrap/expect Ok/Some'u açar, aksi halde panic! eder
  • expect daima tercih edilir: anlamlı, bağlam taşıyan panik mesajı verir
  • Prototip/test/gerçekten imkânsız durumda kabul; kütüphanede ve girdiye bağlı yerlerde kaçın
  • unwrap_or / unwrap_or_else panik yerine varsayılana düşmenin yolu

06 panic! ne zaman?

panic! kurtarılamaz hatalar içindir: programın bir invariant'ı ihlal edildi ve devam etmek anlamsız ya da tehlikeli. C'deki abort() veya assert() başarısızlığının karşılığıdır — ama Rust'ta varsayılan davranış stack'i temiz biçimde geri sarar.

Tipik panik kaynakları

main.rs
let v = vec![1, 2, 3];
let x = v[99];        // panic: index out of bounds — C'de UB, burada kontrollü çökme

let a: i32 = 1;
let b: i32 = 0;
let c = a / b;        // panic: attempt to divide by zero

fn hesapla(adim: u32) {
    if adim == 0 {
        panic!("adim sıfır olamaz — çağıran tarafta hata var");
    }
    // ... invariant artık garanti
}

Dizilim sınırı aşımı C'de tanımsız davranıştır (sessiz bellek bozulması, exploit yüzeyi). Rust'ta panik ile kontrollü biçimde durur; bellek güvenliği korunur.

panic = unwind vs panic = abort

Panik anında iki strateji var. Bunlar Cargo.toml'da profil bazında seçilir:

Cargo.toml
[profile.release]
panic = "abort"    # anında sonlandır; unwind kodu üretilmez
# panic = "unwind"  (varsayılan): stack geri sarılır, Drop'lar çalışır
StratejiDavranış
unwindStack geri sarılır, her frame'in Drop'u çalışır (C++ unwinding gibi). catch_unwind ile yakalanabilir.
abortAnında sonlandırır, Drop çalışmaz. Daha küçük ikili, gömülü/no_std'de yaygın.
DİKKAT

Kütüphane yazarken panic!'ten kaçın. Panik çağıranın kararıdır — kütüphane hatayı Result ile bildirmeli, programı sonlandırma yetkisini gasp etmemeli. Panik yalnızca "bu noktaya gelmek programlama hatasıdır" diyebileceğin invariant ihlallerinde meşrudur.

Bu bölümde

  • panic! kurtarılamaz invariant ihlalleri içindir; assert/abort'un karşılığı
  • Index out of bounds ve sıfıra bölme C'de UB iken Rust'ta kontrollü panik
  • panic = unwind (Drop çalışır, yakalanabilir) vs abort (anında, küçük ikili)
  • Kütüphanede panic'ten kaçın, hatayı Result ile çağırana bırak

07 Hata tiplerini dönüştürmek

Bir fonksiyon farklı kaynaklardan hata alabilir — IO, parse, ağ. ? operatörü, From trait'i sayesinde kaynak hatayı fonksiyonun hata tipine otomatik dönüştürür. Böylece çeşitli hatalar tek bir tipte toplanır.

? + From ile otomatik dönüşüm

?, Err(e) dönerken aslında Err(From::from(e)) çağırır. Hedef hata tipi için From<KaynakHata> tanımlıysa dönüşüm bedava gelir:

main.rs
use std::num::ParseIntError;
use std::io;

// Tüm hataları toplayan kendi enum'umuz
enum UygHata {
    Io(io::Error),
    Parse(ParseIntError),
}

// From impl'leri ?'un dönüşümü için gereken köprü
impl From<io::Error> for UygHata {
    fn from(e: io::Error) -> Self { UygHata::Io(e) }
}
impl From<ParseIntError> for UygHata {
    fn from(e: ParseIntError) -> Self { UygHata::Parse(e) }
}

fn oku_sayi(yol: &str) -> Result<i32, UygHata> {
    let s = std::fs::read_to_string(yol)?;  // io::Error → UygHata
    let n = s.trim().parse::<i32>()?;        // ParseIntError → UygHata
    Ok(n)
}

Box<dyn Error> ile basit toplama

Her hata tipi için enum + From yazmak istemiyorsan, Box<dyn Error> bir kestirmedir. Error trait'ini uygulayan her tip otomatik olarak buna dönüşür:

main.rs
use std::error::Error;

// Box<dyn Error>: "Error uygulayan herhangi bir hata" — type erasure
fn oku_sayi(yol: &str) -> Result<i32, Box<dyn Error>> {
    let s = std::fs::read_to_string(yol)?;  // io::Error → Box<dyn Error>
    let n = s.trim().parse::<i32>()?;        // ParseIntError → Box<dyn Error>
    Ok(n)
}
NOT

Tercih kuralı: kütüphanelerde somut/enum hata tipi (çağıran hatayı ayırt edebilsin, thiserror bunu kolaylaştırır), uygulamalarda Box<dyn Error> ya da anyhow::Error (hızlı, ayrıntı gerekmez). Box<dyn Error> type erasure yapar: hangi somut hata olduğunu kaybedersin, sadece mesajını alırsın.

Bu bölümde

  • ? operatörü Err'i From::from ile hedef hata tipine otomatik dönüştürür
  • From<KaynakHata> impl'leri farklı hataları tek enum'da toplamanın köprüsü
  • Box<dyn Error> type erasure ile From yazmadan hızlı toplama sağlar
  • Kütüphanede somut/enum hata, uygulamada Box<dyn Error>/anyhow tercih edilir

08 Kombinatorlar

match her zaman gerekmez. Option ve Result, akıcı zincirleme için bir dizi kombinatör metodu sunar: map, map_err, and_then, or_else, ok_or, unwrap_or ve dostları. Bunlar hata akışını tek bir ifadeye sıkıştırır.

Sık kullanılanlar

map(f)Ok/Some içindeki değeri dönüştürür, Err/None'a dokunmaz
map_err(f)Err içindeki hatayı dönüştürür (hata tipi uyarlama)
and_then(f)Ok/Some ise f'i çalıştırır, f de Result/Option döner (zincirleme, flatMap)
or_else(f)Err/None ise alternatif üretir
ok_or(e)Option'ı Result'a çevirir: Some→Ok, None→Err(e)
unwrap_or(d)Ok/Some değerini, yoksa d varsayılanını verir (panik yok)
unwrap_or_else(f)Varsayılanı tembel hesaplar: yalnızca gerekirse f çalışır

Zincirleme: match'siz akıcı stil

main.rs
// "  42 " → 84; her adım önceki Ok'a uygulanır, ilk Err zinciri keser
fn iki_kati(s: &str) -> Result<i32, String> {
    s.trim()
        .parse::<i32>()                       // Result<i32, ParseIntError>
        .map(|n| n * 2)                       // Ok değerini ikiye katla
        .map_err(|e| format!("geçersiz: {e}"))  // hata tipini String'e çevir
}

// Option zinciri: config map'inden oku, yoksa varsayılan
fn port(cfg: &std::collections::HashMap<String, String>) -> u16 {
    cfg.get("port")                  // Option<&String>
        .and_then(|s| s.parse().ok())  // Option<u16> (parse hatası → None)
        .unwrap_or(8080)               // yoksa 8080
}

// ok_or: yokluğu anlamlı bir hataya çevir, sonra ? ile yay
fn ilk(v: &[i32]) -> Result<i32, String> {
    let &x = v.first().ok_or("boş dilim".to_string())?;
    Ok(x)
}
NOT

unwrap_or argümanını her zaman hesaplar; pahalı bir varsayılan için unwrap_or_else(|| ...) kullan, böylece kapanış yalnızca gerçekten gerektiğinde çalışır. Aynı tembellik ayrımı C++'taki value_or ile elden hesaplama arasındaki farkın karşılığıdır.

Bu bölümde

  • map/map_err değeri ya da hatayı dönüştürür; varyantı değiştirmez
  • and_then zincirleme (flatMap), or_else alternatif üretme içindir
  • ok_or Option→Result köprüsü; unwrap_or/unwrap_or_else paniksiz varsayılan
  • Kombinatörler match boilerplate'ini akıcı tek ifadeye indirir

09 Gerçek örnek

Öğrendiklerimizi tek bir programda birleştirelim: bir dosyayı oku, satırları parse et, her satırı sayıya çevir, topla. Her adımda ? ile hata yay; tüm hataları main() -> Result<(), Box<dyn Error>> deseninde topla.

Akış

dosya oku  →  satırlara böl  →  her satırı parse et  →  topla  →  yazdır
    ?              (iter)            ? (her satır)        Σ

Tam program

main.rs
use std::error::Error;
use std::fs;

// sayilar.txt: her satırda bir tam sayı. Toplamlarını döndürür.
fn dosyadan_topla(yol: &str) -> Result<i64, Box<dyn Error>> {
    let icerik = fs::read_to_string(yol)?;   // io::Error yayılır

    let mut toplam: i64 = 0;
    for (i, satir) in icerik.lines().enumerate() {
        let t = satir.trim();
        if t.is_empty() { continue; }     // boş satırları atla

        // parse hatasını satır numarasıyla zenginleştir, sonra ? ile yay
        let n: i64 = t.parse()
            .map_err(|e| format!("satır {}: '{}' sayı değil ({e})", i + 1, t))?;
        toplam += n;
    }
    Ok(toplam)
}

// main de Result döner → fonksiyon gövdesinde ? serbest
fn main() -> Result<(), Box<dyn Error>> {
    let toplam = dosyadan_topla("sayilar.txt")?;
    println!("toplam = {toplam}");
    Ok(())   // başarı; main Err dönerse süreç != 0 ile çıkar ve hatayı basar
}

Bu programda hata yönetiminin tüm parçaları bir arada: read_to_string'in io::Error'ı ve parse'ın ParseIntError'ı, Box<dyn Error> sayesinde tek bir dönüş tipinde toplandı; map_err ile hata mesajı satır bağlamıyla zenginleşti; ? hata yaymayı sade tuttu. C eşdeğeri için her fopen, fgets, strtol sonrası elle errno/dönüş kontrolü ve manuel temizlik gerekirdi.

NOT

main'in Err dönmesi durumunda Rust runtime hatayı Debug formatıyla stderr'e basar ve süreç sıfırdan farklı bir kodla çıkar — tıpkı C'de return EXIT_FAILURE gibi, ama hata içeriği otomatik raporlanır. Daha zengin bağlam ve hata zincirleri için anyhow krate'i bu deseni genişletir.

Bu bölümde

  • read → parse → topla akışında her adımda ? ile hata yayıldı
  • Farklı hata tipleri Box<dyn Error> altında tek dönüş tipinde toplandı
  • map_err ile hata mesajı satır numarası bağlamıyla zenginleştirildi
  • main() -> Result<(), Box<dyn Error>> deseni hatayı runtime'a teslim eder