A programok életében keletkezhetnek olyan helyzetek, amikor egy kérést nem tudnak végre hajtani. Ha például egy API-t fejlesztünk, ami kapcsolódik egy adatbázishoz, akkor előfordulhat, hogy az adatbázis szerver éppen nem elérhető. Ilyen helyzetekben hibakezelés nélkül az egyetlen lehetőségünk a pánik, ami rontja a felhasználói élményt és váratlan állapotokat teremthet a rendszerünkben, mivel a program futásának azonnali megszakításával jár.

Az egyik kedvenc funkcióm a rust nyelvben, hogy jelentősen megkönnyíti a hibakezelést. A nyelv kifejezőerejénél fogva világosan rámutat azokra a pontokra, ahol kivételek keletkezhetnek.

Tegyük fel, hogy szeretném inicializálni az adatbázis kapcsolatokat a programomban. Ezt megtehetem a PgPoolManager-em new függvényével:

 1impl PgPoolManager {
 2    pub async fn new(
 3        main_database_config: &BasicDatabaseConfig,
 4        default_tenant_database_config: &BasicDatabaseConfig,
 5    ) -> Result<PgPoolManager, sqlx::Error> {
 6        let main_pool = PgPoolOptions::new()
 7            .max_connections(main_database_config.max_pool_size())
 8            .acquire_timeout(Duration::from_secs(3))
 9            .connect(&main_database_config.url())
10            .await?;
11        let default_tenant_pool = PgPoolOptions::new()
12            .max_connections(default_tenant_database_config.max_pool_size())
13            .acquire_timeout(Duration::from_secs(3))
14            .connect(&default_tenant_database_config.url())
15            .await?;
16        Ok(Self {
17            main_pool,
18            default_tenant_pool,
19            tenant_pools: Arc::new(RwLock::new(HashMap::new())),
20        })
21    }
22}

A rust típusrendszere miatt a függvény szignatúrájából látszik, hogy ez egy olyan művelet, ami bizonyos esetekben meghiúsulhat, mert a függvény visszatérési értéke a standard library Result típusába van csomagolva. (Ami az eredeti kódban egy anyhow::Result, de ez majd egy későbbi bejegyzés témája lesz :))

Ezekkel az információkkal felvértezve a fejlesztő tudja, hogy ezt a függvényt csak úgy hívhatja meg, ha megírja hozzá a hibakezelést is.

Ebben a példában, ha nem sikerül csatlakozni az adatbázishoz, a program értesíti az adminisztrátort a hibáról a felhasználónak pedig kiírni egy szépen formázott üzenetet, ami közli ezt a tényt:

 1
 2#[tokio::main]
 3async fn main(config: Arc<AppConfig>) -> Result<(), String> {
 4    let config = init_config()?;
 5    let pool_manager = PgPoolManager::new(
 6        config.main_database(),
 7        config.default_tenant_database()
 8    ).await;
 9
10    match pool_manager {
11        Ok(pool_manager) => {
12            pool_manager.some_useful_stuff();
13            Ok(())
14        }
15        Err(e) => {
16            error_notify_admin(e);
17            Err(String::from("Nem sikerült csatlakozni az adatbázishoz"))
18        }
19    }
20}

Csak egy kis pánik Link to heading

Sok más programozási nyelvvel ellentétben a rust alapértelmezetten megköveteli a hibakezelést, de ad lehetőséget arra, hogy explicit azt mondjam neki, hogy szeretném kihagyni azt. Ezt az .unwrap() függvénnyel tudom megtenni, ami jól látható módon jelzi a forráskódban, hogy ez történt és szomorú kiskutya szemekkel néz rád, amíg le nem kezeled a hibát.

A .unwrap() használata production-ben erősen ellenjavallott, mert ha error ágra fut, akkor a program azonnal eldob mindent a kezéből és leáll (vagy legalább az éppen futó szálat eldobja anélkül, hogy a felhasználót tájékoztatná hogy mi történt)