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

Hata Crate'leri
anyhow vs thiserror

Uygulama ve kütüphane için hata tasarımı; anyhow ile dinamik hata, thiserror ile tipli hata — std'nin Error trait'i üzerine ergonomik katmanlar.

00 std Error'ın sınırları

std'nin Error trait'i ve ? operatörü hata akışını taşımak için yeterli iskeleti verir, ama gerçek bir projede elle enum + Display + From yazmak hızla angaryaya döner. Ekosistem bu boilerplate'i iki crate ile çözer; ve kitabın ana ayrımı buradan doğar: kütüphane tipli hata (thiserror) döner, uygulama dinamik hata (anyhow) ile sarar.

Elle yazmanın yorgunluğu

Birden fazla alt-hata türünü tek bir tipte toplamak istediğin an, std ile yapman gereken üç ayrı parçayı elle yazarsın: hata enum'u, her varyant için Display, ve her kaynaktan From dönüşümü. Aşağıdaki blok yalnızca iki kaynağı sarmak için ne kadar tören gerektiğini gösteriyor:

handwritten.rs
use std::fmt;

// 1) Hata enum'u
enum ConfigError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

// 2) Display — kullanıcıya görünen mesaj, elle
impl fmt::Display for ConfigError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            ConfigError::Io(e)    => write!(f, "io hatası: {e}"),
            ConfigError::Parse(e) => write!(f, "parse hatası: {e}"),
        }
    }
}
impl fmt::Debug for ConfigError { /* yine elle... */ }
impl std::error::Error for ConfigError {}

// 3) From — ? operatörünün çalışması için her kaynak ayrı ayrı
impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self { ConfigError::Io(e) }
}
impl From<std::num::ParseIntError> for ConfigError {
    fn from(e: std::num::ParseIntError) -> Self { ConfigError::Parse(e) }
}

İki kaynak için ~30 satır. On varyantlı bir hata tipinde bu kod patlar; her yeni alt-hata üç noktada düzenleme ister. Tören tekrarlı, mantık yok — tam derive makrosu işi.

Ekosistemin iki cevabı

Topluluk bu iki ucu iki crate ile karşılıyor. İkisi de std Error trait'inin üstünde oturur, onu değiştirmez:

CrateNe üretirKim için
thiserrorTipli, eşleştirilebilir hata enum'u (derive ile)Kütüphane — public hata API'si
anyhowTek bir dinamik hata tipi (anyhow::Error)Uygulama — hatayı raporla, geç
NOT

Bu ikisi rakip değil, tamamlayıcı. Kütüphanen thiserror ile tipli hata döner; o kütüphaneyi kullanan uygulama anyhow ile her şeyi tek tipte toplar. Hangisini ne zaman seçeceğinin tam kuralı s8'de.

Bu bölümde

  • Elle enum + Display + From yazmak tekrarlı boilerplate üretir
  • Her yeni alt-hata üç ayrı noktada düzenleme gerektirir — derive işi
  • Ekosistem çözümü: thiserror (tipli) ve anyhow (dinamik)
  • Ana ayrım: kütüphane = thiserror, uygulama = anyhow

01 std::error::Error trait'i hatırlatma

İki crate'i anlamak için altlarındaki sözleşmeyi bir kez netleştirelim. std Error trait'i üç şeyi vaat eder: Display (insan mesajı), Debug (geliştirici mesajı) ve source() (alttaki hataya zincir). thiserror tam olarak bu üçünü, anyhow ise bu trait'i implemente eden herhangi bir tipi hedefler.

Trait'in şekli

std_error.rs
// std::error::Error — sadeleştirilmiş hali
pub trait Error: Debug + Display {
    // Hata zincirinin bir alt halkası; varsayılan None
    fn source(&self) -> Option<&(dyn Error + 'static)> { None }
}

Display son kullanıcının gördüğü satırdır; source() ise "bu hataya ne sebep oldu" sorusunun cevabı — IO hatasını saran bir parse hatası gibi. Bir hata aracı (örn. anyhow'un raporlayıcısı) source() zincirini gezerek tam nedensellik dökümünü basar.

? ve From bağlantısı

? operatörü sihirli değil: E1: From<E2> ilişkisi varsa, Result<_, E2>'yi Result<_, E1> döndüren bir fonksiyonda otomatik dönüştürür. Yani s0'daki elle From impl'lerinin tek amacı ?'yi beslemekti:

question_mark.rs
fn oku() -> Result<u32, ConfigError> {
    let s = std::fs::read_to_string("port.txt")?; // io::Error → ConfigError (From)
    let n: u32 = s.trim().parse()?;               // ParseIntError → ConfigError (From)
    Ok(n)
}

Neden ek katman istiyoruz

std her şeyi verir ama hiçbir şeyi üretmez: Display metnini, From impl'lerini, source() bağlamasını elle yazarsın. İstediğimiz iki şey var — kütüphanede tip güvenliğini korurken bu kodu üretmek (thiserror), uygulamada ise tip ayrıntısını umursamadan her hatayı tek potada toplamak (anyhow).

NOT

source() zincirini elle gezmeye gerek yok; err.source()'u takip eden hazır yardımcılar var (örn. anyhow'un {:#} formatı ya da std nightly'deki Error::sources() iteratörü). Zinciri kurmak senin işin, gezmek aracın.

Bu bölümde

  • Error trait'i: Display + Debug + source()
  • source() hata zincirinin alt halkasını verir — nedensellik dökümü
  • ? operatörü From<E> üzerinden otomatik dönüşüm yapar
  • Ek katman: thiserror bu kodu üretir, anyhow tipi tek potada toplar

02 Box<dyn Error> ile basit toplama

Crate eklemeden önce std'nin kendi kestirme yolu var: Box<dyn Error>. Farklı hata tiplerini tek bir trait nesnesinde toplar, ? ile her şeyi yutar. Hızlı ve sıfır bağımlılık — ama tip bilgisini siler, bu yüzden çağıran hatayı match ile eşleştiremez.

Tek satırlık çözüm

Box<dyn Error> her std hatasından otomatik From'a sahip olduğu için ? doğrudan çalışır; ekstra enum ya da From impl'i yazmazsın:

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

// Box<dyn Error>: "Error implemente eden herhangi bir tip"
fn port_oku() -> Result<u32, Box<dyn Error>> {
    let s = std::fs::read_to_string("port.txt")?; // io::Error yutuldu
    let n: u32 = s.trim().parse()?;               // ParseIntError yutuldu
    if n == 0 {
        // &str de Box<dyn Error>'e dönüşür — ad hoc mesaj
        return Err("port 0 olamaz".into());
    }
    Ok(n)
}

Bedeli: tip bilgisi kaybı

Dönen değer artık "bir hata" — hangisi olduğu derleme zamanında bilinmez. Çağıran "dosya mı yoktu, sayı mı bozuktu" ayrımını yapamaz; en fazla Display metnini okur ya da çalışma anında downcast_ref ile tahmin yürütür (kırılgan):

caller.rs
match port_oku() {
    Ok(n)  => println!("port = {n}"),
    // e: Box<dyn Error> — match ile io vs parse ayrımı YOK
    Err(e) => eprintln!("hata: {e}"),
}
DİKKAT

Box<dyn Error> varsayılan olarak Send + Sync değildir; thread'ler arası taşımak ya da çoğu async runtime'da kullanmak için Box<dyn Error + Send + Sync> yazman gerekir. anyhow::Error bu sınırı zaten içerir — bir sebebi daha.

Ne zaman yeter, ne zaman yetmez

DurumBox<dyn Error> uygun mu
Hızlı prototip, kısa mainEvet — sıfır bağımlılık, anında
Çağıranın hata türüne göre dallanması gerekHayır — tip bilgisi yok
Public kütüphane API'siHayır — eşleştirilemez, semver belirsiz
Bağlam/backtrace istiyorsunHayır — anyhow daha iyisini verir

Bu bölümde

  • Box<dyn Error> farklı hataları tek trait nesnesinde toplar, sıfır bağımlılık
  • ? ve .into() ile her std hatası ve &str yutulur
  • Tip bilgisi silinir — çağıran match ile ayrım yapamaz
  • Varsayılan olarak Send + Sync değil; prototip için yeter, API için yetmez

03 thiserror — temel

thiserror, s0'daki üç parçalı el işini tek bir #[derive(Error)] ile üretir. Sen yalnızca enum'u ve her varyantın #[error("...")] mesajını yazarsın; Display, Error impl'leri ve alan enterpolasyonu makro tarafından gelir. Üretilen tip tamamen senin tipindir — public API olarak güvenle döndürülür.

Cargo.toml

thiserror bir derive crate'idir; runtime maliyeti yoktur, tek satır bağımlılık:

Cargo.toml
[dependencies]
thiserror = "2"

İlk hata enum'u

#[error("...")] stringi o varyantın Display çıktısıdır. İçinde alanlara konumla ({0}) veya isimle ({path}) erişebilirsin — format! ile aynı söz dizimi:

error.rs
use thiserror::Error;

// Derive: Display + Error impl'leri otomatik üretilir
// Debug'ı kendin türetirsin — trait sınırı bunu ister
#[derive(Error, Debug)]
pub enum ConfigError {
    // {0} → tuple alanı 0; Display: "geçersiz port: 70000"
    #[error("geçersiz port: {0}")]
    GecersizPort(u32),

    // {path}, {line} → isimli alanlar
    #[error("eksik anahtar '{key}' (satır {line})")]
    EksikAnahtar { key: String, line: usize },

    // Alansız varyant — sabit mesaj
    #[error("yapılandırma boş")]
    Bos,
}

Kullanımı sıradan bir enum gibidir, ama artık Display ve Error bedavadır:

usage.rs
fn port_dogrula(p: u32) -> Result<u32, ConfigError> {
    if p > 65535 {
        return Err(ConfigError::GecersizPort(p));
    }
    Ok(p)
}

// println!("{}", err) → "geçersiz port: 70000"
{0}, {1}Tuple varyantta konuma göre alan enterpolasyonu
{field}Struct varyantta isme göre alan enterpolasyonu
#[error(...)]O varyantın Display mesajını tanımlar
NOT

#[error("...")] biçim dizesi format! ile aynı söz dizimini kullanır: {0} tuple alanını konumuyla, {field} struct alanını ismiyle yazdırır; ifadelere self üzerinden erişebilirsin. thiserror hiçbir runtime maliyeti getirmez — yalnızca elle yazacağın Display ve Error impl'lerini derive zamanında üretir.

Bu bölümde

  • #[derive(Error, Debug)] Display + Error impl'lerini üretir
  • #[error("...")] her varyantın Display mesajını tanımlar
  • Alan enterpolasyonu: {0} konumla, {field} isimle
  • Üretilen tip senin public tipin — runtime maliyeti yok, derive zamanı

04 thiserror — #[from] ve source

Asıl güç burada: #[from] niteliği bir alan için hem From dönüşümünü hem de source() bağlamasını otomatik üretir. Böylece alt-hataları sararken ? doğrudan çalışır ve hata zinciri korunur — s0'da elle yazdığın iki From impl'i tek satıra iner.

#[from] ile otomatik From + ?

#[from] işaretli alan, o tipten From impl'ini doğurur; aynı zamanda o alanı source() olarak bağlar. Yani ? hem dönüştürür hem zinciri kurar:

from.rs
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    // #[from]: From<io::Error> üretir + source() olarak bağlar
    // {0} ile alttaki hatanın mesajını da gösterebiliriz
    #[error("dosya okunamadı: {0}")]
    Io(#[from] std::io::Error),

    #[error("sayı çözümlenemedi: {0}")]
    Parse(#[from] std::num::ParseIntError),
}

fn port_oku() -> Result<u32, ConfigError> {
    let s = std::fs::read_to_string("port.txt")?; // io::Error → ConfigError::Io
    let n: u32 = s.trim().parse()?;               // ParseIntError → ::Parse
    Ok(n)
}

Elle yazdığımız ~30 satır artık ~10. Hem From hem source() ücretsiz — ve çağıran hâlâ match ile Io mu Parse mı ayrımını yapabilir.

#[source] ve transparent

Bazen alttaki hatayı From ile değil, ek bağlamla saran açık bir kurucudan üretmek istersin; o zaman #[from] yerine sade #[source] kullanırsın (sadece zinciri kurar, From üretmez). Ve eğer bir varyant alttaki hatayı hiç değiştirmeden iletiyorsa, #[error(transparent)] hem Display'i hem source()'u doğrudan içteki hataya devreder:

source.rs
#[derive(Error, Debug)]
pub enum ConfigError {
    // #[source]: zinciri kurar ama From üretmez — ek bağlam eklersin
    #[error("'{path}' okunamadı")]
    Oku {
        path: String,
        #[source] kaynak: std::io::Error,
    },

    // transparent: Display ve source() içteki hataya birebir devredilir
    // "kendi mesajı olmayan, sadece taşıyan" varyantlar için
    #[error(transparent)]
    Diger(#[from] std::io::Error),
}

İç içe hata zinciri

thiserror'un kurduğu source() bağları, raporlama anında tam nedensellik zincirini verir. anyhow'un {:#} formatı ya da bir log aracı bu zinciri tek satırda gezer:

ConfigError::Oku  →  source: io::Error("permission denied")
"'app.toml' okunamadı: permission denied (os error 13)"
NOT

Bir enum'da aynı kaynak tipinden iki varyantta #[from] kullanamazsın — From<T> tek olmalı, yoksa hangi varyanta gideceği belirsizdir (derleme hatası). O durumda birinde #[from], diğerinde #[source] + açık kurucu kullan.

Bu bölümde

  • #[from] hem From dönüşümünü hem source() bağını üretir — ? doğrudan çalışır
  • #[source] sadece zinciri kurar, From üretmez — ek bağlam eklemek için
  • #[error(transparent)] Display/source()'u içteki hataya devreder
  • Aynı kaynak tipinden iki #[from] yasak — biri #[source] olmalı

05 Kütüphane hata enum'u tasarımı

Kütüphane yazıyorsan hatan API'nin bir parçasıdır; çağıran ona göre dallanır. Hedef: public, tipli, eşleştirilebilir bir hata enum'u. thiserror tam bunu üretir — ama varyant listesi semver'i etkilediği için tasarımı bilinçli yapmak gerekir. #[non_exhaustive] burada anahtar.

İyi tasarlanmış public hata

Her varyant çağıranın gerçekten ayırt etmek isteyebileceği bir durumu temsil etmeli; iç implementasyon ayrıntısını sızdırmamalı. Düşük seviye hataları (io::Error) source() olarak sar, ama varyantı kendi domain'inde adlandır:

lib_error.rs
use thiserror::Error;

// #[non_exhaustive]: çağıran match'i _ kolu olmadan kapatamaz
// → ileride yeni varyant eklemek BREAKING change olmaz
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum DbError {
    #[error("bağlantı kurulamadı: {0}")]
    Connect(#[from] std::io::Error),

    #[error("sorgu zaman aşımına uğradı ({ms} ms)")]
    Timeout { ms: u64 },

    #[error("kayıt bulunamadı: id={0}")]
    NotFound(u64),
}

Çağıran tarafta bu tip tam tip güvenliğiyle eşleştirilir — Box<dyn Error>'in veremediği şey:

consumer.rs
match db.kayit_getir(id) {
    Ok(k) => islet(k),
    // Domain'e göre dallanma — tipli hatanın asıl değeri
    Err(DbError::NotFound(_)) => varsayilan_kayit(),
    Err(DbError::Timeout { .. }) => tekrar_dene(),
    // non_exhaustive → bu _ kolu ZORUNLU
    Err(e) => return Err(e.into()),
}

Semver etkisi

Değişikliknon_exhaustive yoksanon_exhaustive varsa
Yeni varyant eklemekBREAKING (minor olamaz)Uyumlu (minor)
Varyant alanına alan eklemekBREAKINGAlan da non_exhaustive ise uyumlu
Varyant silmek/yeniden adlandırmakBREAKINGBREAKING
DİKKAT

#[from] ile sardığın alt-hata tipi public API'nin parçası olur: From<io::Error>'ı sızdırırsın. O kaynağı ileride değiştirmek (örn. io::Error yerine başka bir tip) BREAKING olur. Düşük seviye tipleri public yüzeyden gizlemek istersen #[source] + kendi kurucun ile sar, #[from]'u zorlama.

Bu bölümde

  • Kütüphane hatası public API'dir — tipli ve eşleştirilebilir olmalı
  • Varyantlar domain durumlarını temsil etmeli, implementasyon ayrıntısını değil
  • #[non_exhaustive] ileride varyant eklemeyi semver-uyumlu kılar
  • #[from] kaynak tipini public yüzeye sızdırır — bilinçli kullan

06 anyhow — temel

Uygulama tarafında hatayı genelde raporlar ve çıkarsın, türüne göre dallanmazsın. anyhow tam bunun için: tek bir anyhow::Error tipi, Error implemente eden her hatayı ? ile yutar. Box<dyn Error>'in ergonomik, Send + Sync ve backtrace'li versiyonu.

Cargo.toml

Cargo.toml
[dependencies]
anyhow = "1"

anyhow::Result ve ? ile yutma

anyhow::Result<T> aslında Result<T, anyhow::Error> için bir takma addır. anyhow::Error her E: Error + Send + Sync + 'static'ten From'a sahip olduğu için farklı kaynaklardan gelen hataları tek imzada ? ile toplarsın — elle From, elle enum yok:

app.rs
use anyhow::Result; // = Result<T, anyhow::Error>

fn ayar_yukle() -> Result<u32> {
    // io::Error, ParseIntError, hatta DbError... hepsi tek tipte toplanır
    let s = std::fs::read_to_string("port.txt")?;
    let n: u32 = s.trim().parse()?;
    Ok(n)
}

main()'de hızlı kullanım

main imzasını anyhow::Result<()> yaparsan ?'yi doğrudan main'de kullanabilirsin; hata olursa runtime onu Debug formatıyla (zincir + varsa backtrace) basar ve exit kodu döner:

main.rs
use anyhow::Result;

fn main() -> Result<()> {
    let port = ayar_yukle()?; // hata → main'den çıkar, otomatik raporlanır
    println!("dinleniyor: {port}");
    Ok(())
}

Hata anında tipik çıktı, alttaki gerçek hatayı korur: Error: invalid digit found in string. s7'de bunu context ile anlamlı katmana çevireceğiz.

NOT

anyhow::Error tek bir somut tiptir, trait nesnesi değil; bu yüzden boyut sabit (tek pointer) ve Send + Sync + 'static garantili — async görevlerde ve thread'ler arasında sorunsuz taşınır. Box<dyn Error>'in pratikteki üstün sürümü olarak düşün.

Bu bölümde

  • anyhow::Result<T> = Result<T, anyhow::Error> takma adı
  • anyhow::Error her Error + Send + Sync + 'static'i ? ile yutar
  • main() -> Result<()> ile hatayı otomatik raporlat
  • Tek somut tip, sabit boyut, Send + Sync — async/thread güvenli

07 context ve bağlam

Çıplak bir invalid digit found in string kullanıcıya hiçbir şey anlatmaz: hangi dosya, hangi adım? anyhow'un .context() / .with_context() metotları her ? noktasına anlamlı bir katman ekler ve alttaki hatayı source() olarak korur. bail! ve ensure! makroları ise erken çıkışı kısaltır.

.context() ile katman ekleme

.context() sabit bir mesaj ekler; mesajı üretmek pahalıysa (string formatlama) .with_context() ile tembel kapanış kullan — yalnızca hata olduğunda çalışır:

context.rs
use anyhow::{Result, Context};

fn ayar_yukle(path: &str) -> Result<u32> {
    let s = std::fs::read_to_string(path)
        // .with_context: mesaj tembel, sadece hata olunca üretilir
        .with_context(|| format!("'{path}' okunamadı"))?;

    let n: u32 = s.trim().parse()
        // .context: sabit mesaj, hemen değerlenir
        .context("port bir sayı değil")?;

    Ok(n)
}

Artık {:#} formatıyla basıldığında tüm zincir okunur hale gelir:

'app.toml' okunamadı: No such file or directory (os error 2)
port bir sayı değil: invalid digit found in string

bail! ve ensure!

bail! ad hoc bir hatayla erken döner (return Err(anyhow!(...)) kısaltması); ensure! bir koşulu kontrol eder, sağlanmazsa bail! eder — C'deki assert'in kurtarılabilir, mesajlı sürümü:

macros.rs
use anyhow::{Result, bail, ensure};

fn port_dogrula(p: u32) -> Result<u32> {
    // ensure!: koşul false ise anyhow!(mesaj) ile döner
    ensure!(p != 0, "port 0 olamaz");

    if p > 49151 {
        // bail!: koşulsuz erken çıkış, ad hoc mesaj
        bail!("port {p} ayrılmış aralıkta");
    }
    Ok(p)
}

Backtrace

anyhow, RUST_BACKTRACE=1 ortam değişkeni ayarlıysa hata oluştuğu noktada backtrace yakalar ve {:#} / Debug çıktısına ekler. Kaynak değişikliği gerekmez — yalnızca ortam değişkeni. Bu, C'de gdb ile peşinden koştuğun "nereden geldi" sorusunu hatanın kendisine gömer.

.context(m)Sabit mesajla bağlam ekler, hemen değerlenir
.with_context(f)Tembel mesaj; kapanış yalnızca hata olunca çalışır
bail!(m)Koşulsuz return Err(anyhow!(m))
ensure!(c, m)Koşul sağlanmazsa bail!(m)
NOT

Pahalı bir mesaj (string interpolasyonu, allocation) için her zaman .with_context(|| ...) seç; .context(format!(...)) mesajı hata olmasa bile her çağrıda üretir — sıcak yolda gereksiz maliyet.

Bu bölümde

  • .context() sabit, .with_context() tembel bağlam ekler; alttaki hata korunur
  • {:#} formatı tüm source() zincirini okunur basar
  • bail! erken çıkış, ensure! koşullu erken çıkış makrosu
  • RUST_BACKTRACE=1 ile hata noktasında backtrace gömülür

08 İkisi birlikte + karar

thiserror ve anyhow rakip değil, katman. Tipik mimari: kütüphane thiserror ile tipli hata döner, uygulama anyhow ile sarar ve bağlam ekler. Gerektiğinde downcast ile dinamik hatadan tipe geri inebilirsin. Bu bölüm net karar kuralını verir.

Katmanlı akış

Kütüphanenin tipli hatası, uygulamanın ?'sinde otomatik anyhow::Error'a dönüşür (anyhow her Error'ı yutar); uygulama üstüne context ekler:

layered.rs
// --- KÜTÜPHANE: tipli, eşleştirilebilir ---
#[derive(thiserror::Error, Debug)]
pub enum CfgError {
    #[error("alan eksik: {0}")]
    Eksik(String),
}

// --- UYGULAMA: dinamik, raporlanabilir ---
use anyhow::{Result, Context};

fn calistir() -> Result<()> {
    // CfgError → anyhow::Error otomatik (?), üstüne bağlam
    let cfg = kutuphane::yukle()
        .context("başlangıç yapılandırması yüklenemedi")?;
    Ok(())
}

downcast ile tipe geri inmek

anyhow tipi sildi sanma — orijinal somut hata içeride duruyor. downcast_ref ile geri inip o tipe özel davranabilirsin (örn. yalnızca belirli bir hatada retry):

downcast.rs
match calistir() {
    Ok(()) => {}
    Err(e) => {
        // anyhow::Error → &CfgError'a geri in
        if let Some(CfgError::Eksik(alan)) = e.downcast_ref::<CfgError>() {
            eprintln!("'{alan}' alanını ekleyin");
        } else {
            eprintln!("hata: {e:#}"); // tüm zincir
        }
    }
}

Net karar tablosu

Soruthiserroranyhow
NeredeKütüphane (lib crate)Uygulama (bin crate)
Çağıran hatayı eşleştirmeli miEvet — tipliHayır — raporla, geç
Hata public API miEvetHayır
Bağlam/backtrace kolaylığıElleYerleşik (context)
Tek tip mi, sayılı varyant mıSayılı, somutTek dinamik tip
NOT

Kural cümlesi: thiserror = lib, anyhow = app. Tereddütteysen "çağıran bu hataya göre dallanacak mı?" diye sor — evetse thiserror (tip lazım), hayırsa anyhow (raporla yeter). Aynı binary içinde alt modüller arasında bile bu ayrım geçerli olabilir.

Bu bölümde

  • Tipik mimari: kütüphane thiserror döner, uygulama anyhow ile sarar
  • thiserror hatası ? ile otomatik anyhow::Error'a dönüşür
  • downcast_ref::<T>() ile dinamik hatadan somut tipe geri inilir
  • Karar kuralı: thiserror = lib, anyhow = app

09 Gerçek örnek

Hepsini birleştiren küçük bir sistem: bir config yükleyici kütüphanesi thiserror ile tipli, eşleştirilebilir hata döner; onu kullanan uygulama anyhow + context ile zincirler ve kullanıcıya okunur bir mesaj basar.

Kütüphane: tipli hata (thiserror)

config/src/lib.rs
use thiserror::Error;

// Public, tipli, ileride genişletilebilir hata API'si
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("yapılandırma dosyası okunamadı")]
    Io(#[from] std::io::Error),

    #[error("'{0}' satırında '=' yok")]
    BicimsizSatir(String),

    #[error("zorunlu anahtar eksik: {0}")]
    EksikAnahtar(String),

    #[error("'port' sayı değil: {0}")]
    GecersizPort(#[from] std::num::ParseIntError),
}

pub struct Config { pub host: String, pub port: u16 }

// Tipli Result döner — çağıran varyanta göre dallanabilir
pub fn yukle(path: &str) -> Result<Config, ConfigError> {
    let metin = std::fs::read_to_string(path)?; // io::Error → ::Io
    let mut host = None;
    let mut port = None;

    for satir in metin.lines().filter(|l| !l.trim().is_empty()) {
        let (k, v) = satir.split_once('=')
            .ok_or_else(|| ConfigError::BicimsizSatir(satir.into()))?;
        match k.trim() {
            "host" => host = Some(v.trim().to_string()),
            "port" => port = Some(v.trim().parse::<u16>()?), // ParseIntError → ::GecersizPort
            _ => {}
        }
    }

    Ok(Config {
        host: host.ok_or_else(|| ConfigError::EksikAnahtar("host".into()))?,
        port: port.ok_or_else(|| ConfigError::EksikAnahtar("port".into()))?,
    })
}

Uygulama: dinamik sarma (anyhow + context)

app/src/main.rs
use anyhow::{Result, Context, ensure};

fn main() -> Result<()> {
    let path = "app.conf";

    // ConfigError → anyhow::Error otomatik (?); üstüne okunur bağlam
    let cfg = config::yukle(path)
        .with_context(|| format!("'{path}' yapılandırması yüklenemedi"))?;

    // Uygulama düzeyi kural — ensure! ile
    ensure!(cfg.port >= 1024, "ayrıcalıklı port {} reddedildi", cfg.port);

    println!("sunucu başlıyor: {}:{}", cfg.host, cfg.port);
    Ok(())
}

Kullanıcının gördüğü çıktı

Dosyada port=abc yazıyorsa, main'in Result<()> raporlayıcısı {:#} ile tüm zinciri basar — uygulama bağlamı en üstte, kütüphanenin tipli hatası altta:

Error: 'app.conf' yapılandırması yüklenemedi
Caused by:
    'port' sayı değil: invalid digit found in string
NOT

Çağıran tipe ihtiyaç duyarsa hâlâ erişebilir: err.downcast_ref::<config::ConfigError>() ile EksikAnahtarGecersizPort mu ayrımını yapıp farklı exit kodu döndürebilir. Tipli hata kütüphanede korundu, dinamik raporlama uygulamada eklendi — iki crate'in tam olarak amaçladığı denge.

DİKKAT

Tersini yapma: kütüphanenden anyhow::Error döndürme. O an çağıran hatanı match edemez, yalnızca metnini okur — public API'ni dinamik ve eşleştirilemez kılarsın. Kütüphane her zaman kendi tipli hatasını döner.

Bu bölümde

  • Kütüphane thiserror ile non_exhaustive, tipli, eşleştirilebilir hata döner
  • Uygulama anyhow + context ile sarar, main'de otomatik raporlar
  • {:#} çıktısı uygulama bağlamı + kütüphane hatası zincirini gösterir
  • Kütüphaneden anyhow döndürmek anti-pattern — tip güvenliğini kaybedersin