Ha C++-ból, Javából vagy Pythonból jössz, valószínűleg megszoktad, hogy a hibák valahol a levegőben lebegnek, és try/catch blokkokkal kapod el őket – vagy nem, és a program elszáll. A Rust ezzel szemben egészen más filozófiát követ: a hibalehetőség a típusrendszer része, nem egy rejtett, láthatatlan kontrollfolyam.

A Result<T, E> típus és miért jobb, mint a kivételek

A Result<T, E> egy egyszerű enum:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Ennyi. Nincs mágia, nincs stack unwinding a háttérben, nincs rejtett teljesítménybüntetés. Ha egy függvény visszaadhat hibát, ez a szignatúrájából azonnal látszik:

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

fn read_file_content(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    Ok(content)
}

fn main() {
    match read_file_content("config.txt") {
        Ok(content) => println!("Fájl tartalma: {content}"),
        Err(e) => eprintln!("Hiba történt: {e}"),
    }
}

Ennek a megközelítésnek több előnye is van a kivételekhez képest:

  • Explicit szerződés: a fordító megköveteli, hogy kezeld (vagy tudatosan elhanyagold) a hibát. Nincs "elfelejtett catch blokk".
  • Nincs rejtett kontrollfolyam: pontosan látod a kódban, hol térhet vissza korán a függvény.
  • Nulla futásidejű overhead: a Result egy sima enum, nincs mögötte exception-mechanizmus.
  • Komponálhatóság: a Result ugyanolyan típus, mint bármi más, tehát map-elhető, and_then-elhető, mintázható.
Figyelem

Ne használj unwrap()-et éles kódban csak azért, mert "biztos nem lesz hiba". A Rust compiler pontosan azért kényszerít a hibakezelésre, hogy elkerüld a futásidejű panicot – az unwrap() ezt az egész biztonsági hálót kidobja az ablakon.

A ? operátor és a hibapropagáció

A fenti read_file_content függvényben már láthattad a ? operátort. Ez a leggyakoribb módja annak, hogy egy hibát feljebb adjunk anélkül, hogy minden lépésnél explicit match-et kellene írnunk.

A ? operátor pontosan azt csinálja, mint egy match:

// Ez:
let file = File::open(path)?;

// ezzel egyenértékű:
let file = match File::open(path) {
    Ok(f) => f,
    Err(e) => return Err(e.into()),
};

A lényeg az .into() hívásban van: a ? operátor automatikusan konvertálja a hibát a célfüggvény hibatípusára, ha van hozzá From implementáció. Ez teszi lehetővé, hogy egy alacsonyabb szintű hibát (mondjuk io::Error) egy saját, magasabb szintű hibatípusra alakítsunk propagálás közben.

Tipp

A ? operátor csak akkor működik, ha a hívó függvény visszatérési típusa Result (vagy Option), és a hibatípusok között van From implementáció (vagy azonosak). Ha ez hiányzik, a fordító azonnal jelez.

Egy apró, de hasznos 2021-es kiegészítés a let-else szintaxis, amivel az early-return mintákat lehet tömörebben leírni feltételek esetén – nem hibapropagáció, de gyakran együtt használjuk:

fn first_word(input: &str) -> &str {
    let Some(word) = input.split_whitespace().next() else {
        return "";
    };
    word
}

Egyedi hibatípusok a thiserror crate-tel

Ha könyvtárat írsz, a std::io::Error vagy egy generikus String hibaüzenet nem elegáns megoldás. Sokkal jobb, ha saját, strukturált hibatípusod van, amit a hívó fél mintázhat (match-elhet).

Itt jön képbe a thiserror crate, ami a boilerplate kódot generálja neked (a Display és az Error trait implementációját):

use thiserror::Error;

#[derive(Debug, Error)]
enum ConfigError {
    #[error("nem sikerült megnyitni a fájlt: {0}")]
    Io(#[from] std::io::Error),

    #[error("hibás formátum a {line}. sorban")]
    ParseError { line: usize },

    #[error("hiányzó kulcs: {0}")]
    MissingKey(String),
}

fn parse_config(raw: &str) -> Result<(), ConfigError> {
    if raw.is_empty() {
        return Err(ConfigError::MissingKey("db_url".to_string()));
    }
    Ok(())
}

A #[from] attribútum automatikusan generál egy From<std::io::Error> for ConfigError implementációt, így a ? operátor működik olyan függvényekben is, amik io::Error-t dobó hívásokat tartalmaznak, de ConfigError-t adnak visszatérési típusként.

Megjegyzés

A thiserror csak makrókat generál fordítási időben, futásidejű overhead nélkül – ezért ez az ajánlott választás library kódhoz, ahol a hívó félnek tudnia kell mintázni a konkrét hibaeseteket.

Az Cargo.toml-ba ennyi elég:

[dependencies]
thiserror = "1.0"

Gyors prototípusozás anyhow-val

Míg a thiserror a strukturált, mintázható hibákra jó, egy alkalmazás (bináris) belsejében gyakran nem érdekel, pontosan milyen hiba történt – csak az, hogy történt-e, és hogy jó legyen a hibaüzenet a logban. Erre találták ki az anyhow crate-et.

use anyhow::{bail, Context, Result};

fn load_settings(path: &str) -> Result<String> {
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("nem tudtam beolvasni: {path}"))?;

    if content.trim().is_empty() {
        bail!("a konfigurációs fájl üres: {path}");
    }

    Ok(content)
}

fn main() -> Result<()> {
    let settings = load_settings("app.toml")?;
    println!("{settings}");
    Ok(())
}

Az anyhow::Result<T> valójában egy Result<T, anyhow::Error> alias, ahol az anyhow::Error bármilyen std::error::Error-t implementáló típust magába tud csomagolni. A .context() és .with_context() metódusokkal pedig egy egész hibaláncot építhetsz fel, ami hibakeresésnél aranyat ér – minden szint hozzáteszi a saját kontextusát a hibaüzenethez.

Az Cargo.toml-ban:

[dependencies]
anyhow = "1.0"

Mikor melyiket válasszuk: könyvtár vs alkalmazás kód

Ez a leggyakoribb kérdés, amit kezdők (és haladók) is feltesznek. A válasz nagyjából egy jól bevált konvenció:

  • Könyvtár (library crate): használj thiserror-t (vagy kézzel írt hibatípust). A hívó félnek joga van tudni, pontosan milyen hiba történt, és esetleg más módon reagálni rá (match ConfigError::Io(_) => ...). Egy library API-jának stabilnak és mintázhatónak kell lennie.
  • Alkalmazás (binary crate): használj anyhow-t. A main függvényben vagy egy CLI eszközben a legtöbbször nem kell megkülönböztetni a hibaforrásokat – elég, ha jó, kontextusban gazdag hibaüzenetet kap a felhasználó vagy a log.
// main.rs egy tipikus CLI alkalmazásban
use anyhow::Result;

fn main() -> Result<()> {
    let config = load_settings("app.toml")?;
    println!("Betöltve: {config}");
    Ok(())
}
# fn load_settings(_: &str) -> Result<String> { Ok(String::new()) }

Sok valós projektben a kettő keveredik: a belső könyvtári modulok thiserror-alapú enumokat exportálnak, a main.rs pedig anyhow::Result-ot használ, és a ? operátor a From implementáció (amit a thiserror generál) miatt zökkenőmentesen konvertál a konkrét hibatípusból az anyhow::Error-ba.

Jó tudni

Ha egy függvény API felszínén van (public, más crate is hívhatja), gondold át kétszer, mielőtt anyhow::Error-t adnál vissza – ezzel elveszed a hívótól a lehetőséget, hogy mintázza a konkrét hibaesetet. anyhow inkább a végfelhasználói kód (bin) belseje, nem a public API.

Összefoglalás

A Rust hibakezelése első ránézésre talán szigorúbbnak tűnik, mint a kivétel-alapú nyelvek, de ez a szigorúság a barátod: a Result<T, E> explicit, komponálható, és zéró rejtett költséggel jár. A ? operátor eltünteti a boilerplate match-eket, a thiserror strukturált, mintázható hibatípusokat ad a könyvtáraidnak, az anyhow pedig gyors, kontextusban gazdag hibakezelést biztosít az alkalmazásod belsejében. Ha ezt a három eszközt együtt használod, a hibakezelésed egyszerre lesz típusbiztos és kényelmes – pontosan az, amire szükséged van egy valódi, produkciós Rust projektben.