Obsługa Błędów w Rust 2026: Result, Option, thiserror i anyhow

Kompleksowy przewodnik po obsłudze błędów w Rust wykorzystujący Result, Option, operator ? oraz biblioteki thiserror i anyhow. Poznaj najlepsze praktyki i wzorce stosowane w produkcyjnych aplikacjach.

Obsługa błędów w Rust z Result, Option, thiserror i anyhow

Obsługa błędów w Rust opiera się na dwóch enumach—Result<T, E> i Option<T>—które wymuszają jawną obsługę sukcesu, porażki i braku wartości już na etapie kompilacji. W przeciwieństwie do wyjątków, które propagują się niewidocznie, podejście Rust sprawia, że ścieżki błędów są widoczne w sygnaturach funkcji, eliminując całe kategorie niespodzianek w czasie wykonania. Operator ? w połączeniu z bibliotekami takimi jak thiserror i anyhow usprawnia ten jawny model bez utraty czytelności.

Kiedy używać którego typu

Typ Option<T> stosuje się dla wartości, które mogą być legalnie nieobecne (pola konfiguracji, wyniki wyszukiwania). Typ Result<T, E> używa się gdy operacje mogą zakończyć się niepowodzeniem z znaczącą informacją o błędzie (operacje I/O na plikach, żądania sieciowe, parsowanie).

Podstawy Result i Option

Result<T, E> reprezentuje sukces (Ok(T)) lub porażkę (Err(E)). Option<T> reprezentuje obecność wartości (Some(T)) lub jej brak (None). Oba są typami sum—kompilator zapewnia obsługę każdego wariantu.

error_examples.rsrust
// Demonstrates Result and Option basic patterns

fn find_user(id: u64) -> Option<String> {
    // Returns None if user doesn't exist
    if id == 0 {
        None
    } else {
        Some(format!("User-{}", id))
    }
}

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    // Returns Err if parsing fails
    s.parse::<u16>()
}

fn main() {
    // Option handling - must address None case
    match find_user(42) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Result handling - must address Err case
    match parse_port("8080") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("Invalid port: {}", e),
    }
}

Kompilator odrzuca kod, który ignoruje te wartości zwracane bez jawnej obsługi. Ten projekt wyłapuje błędy w czasie kompilacji, które w innych językach ujawniałyby się jako wyjątki null pointer lub nieprzechwycone błędy.

Operator Znaku Zapytania dla Zwięzłej Propagacji

Operator ? przekształca rozwlekłe łańcuchy match w czytelny liniowy kod. Zastosowany do Result powoduje wczesny powrót z błędem jeśli jest obecny, lub rozpakowuje wartość sukcesu. To samo dotyczy Option.

file_reader.rsrust
// Using ? for clean error propagation

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let file = File::open(path)?;  // Returns early if open fails
    let mut reader = BufReader::new(file);
    let mut line = String::new();
    reader.read_line(&mut line)?;  // Returns early if read fails
    Ok(line.trim().to_string())
}

fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_first_line(path)?;
    let port = content.parse::<u16>()?;  // ParseIntError converts via From
    Ok(port)
}

Operator ? wymaga, aby typ błędu był konwertowalny do typu błędu zwracanego przez funkcję poprzez trait From. Użycie Box<dyn std::error::Error> jak pokazano powyżej akceptuje dowolny typ błędu implementujący standardowy trait Error.

Tworzenie Własnych Błędów z thiserror

Biblioteka thiserror eliminuje boilerplate dla własnych typów błędów. Derywuje implementacje Error, Display i From poprzez makro proceduralne.

errors.rsrust
// Custom error types using thiserror 2.0

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("configuration file not found at {path}")]
    NotFound { path: String },

    #[error("invalid port number: {0}")]
    InvalidPort(#[from] std::num::ParseIntError),

    #[error("IO error reading config")]
    IoError(#[from] std::io::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;  // IoError auto-converts
    let port: u16 = content
        .lines()
        .find(|l| l.starts_with("port="))
        .ok_or(ConfigError::MissingField("port".into()))?
        .strip_prefix("port=")
        .unwrap()
        .parse()?;  // ParseIntError auto-converts to InvalidPort

    Ok(Config { port })
}

struct Config {
    port: u16,
}

Atrybut #[from] generuje automatyczne konwersje, umożliwiając płynne użycie ? z różnymi typami błędów źródłowych. Komunikaty błędów stają się samodokumentujące poprzez ciągi formatujące #[error(...)].

Błędy na Poziomie Aplikacji z anyhow

Podczas gdy thiserror pasuje do kodu bibliotecznego ze specyficznymi typami błędów, anyhow celuje w aplikacje, gdzie kontekst błędu ma większe znaczenie niż granularność typu. Trait Context dodaje opisowe komunikaty do dowolnego błędu.

main.rsrust
// Application error handling with anyhow 1.0

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

fn load_database_url() -> Result<String> {
    std::env::var("DATABASE_URL")
        .context("DATABASE_URL environment variable not set")
}

fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
    ensure!(!url.is_empty(), "database URL cannot be empty");

    let conn = DatabaseConnection::new(url)
        .context("failed to establish database connection")?;

    if !conn.is_healthy() {
        bail!("database connection unhealthy after establishment");
    }

    Ok(conn)
}

fn main() -> Result<()> {
    let url = load_database_url()?;
    let conn = connect_to_database(&url)
        .context("application startup failed")?;

    // Context chains create readable error traces:
    // Error: application startup failed
    // Caused by:
    //     0: failed to establish database connection
    //     1: connection refused

    Ok(())
}

struct DatabaseConnection;
impl DatabaseConnection {
    fn new(_url: &str) -> Result<Self> { Ok(Self) }
    fn is_healthy(&self) -> bool { true }
}

Metoda context() opakowuje błędy dodatkowymi informacjami, tworząc łańcuch wspierający debugowanie. Makro bail! zapewnia wczesne wyjście ze sformatowanym komunikatem błędu, podczas gdy ensure! działa jako asercja zwracająca błąd zamiast panikować.

Gotowy na rozmowy o Rust?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Łączenie thiserror i anyhow w Rzeczywistych Projektach

Biblioteki eksponują strukturalne błędy przez thiserror dla programowej obsługi przez konsumentów. Aplikacje opakowują te błędy za pomocą anyhow dla czytelnego wyjścia dla człowieka. Ta separacja utrzymuje czyste API przy zachowaniu możliwości debugowania.

lib.rs - Library code with thiserrorrust
// Exposes typed errors for programmatic handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum PaymentError {
    #[error("insufficient funds: required {required}, available {available}")]
    InsufficientFunds { required: u64, available: u64 },

    #[error("card declined: {reason}")]
    CardDeclined { reason: String },

    #[error("payment provider unavailable")]
    ProviderUnavailable(#[source] reqwest::Error),
}

pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
    // Library returns specific, matchable error types
    Err(PaymentError::InsufficientFunds {
        required: amount,
        available: 50,
    })
}

pub struct Receipt;
main.rs - Application code with anyhowrust
// Wraps library errors with context

use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};

fn checkout(cart_total: u64) -> Result<()> {
    match process_payment(cart_total) {
        Ok(_receipt) => Ok(()),
        Err(PaymentError::InsufficientFunds { required, available }) => {
            // Handle specific case differently
            println!("Add {} to your balance", required - available);
            Ok(())
        }
        Err(e) => Err(e).context("checkout payment processing failed"),
    }
}

Ten wzorzec umożliwia wywołującym dopasowanie konkretnych wariantów gdy odzyskanie jest możliwe, jednocześnie korzystając z bogatego kontekstu błędu podczas propagacji niepowodzeń w górę. Społeczność Rust w dużej mierze ustandaryzowała to podejście, jak omówiono w wytycznych Rust API.

Wzorce Obsługi Błędów dla Kodu Asynchronicznego

Funkcje async zwracają Result tak samo jak synchroniczne. Operator ? działa identycznie wewnątrz bloków async, a zarówno thiserror jak i anyhow integrują się bez modyfikacji.

async_errors.rsrust
// Error handling in async Rust with Tokio

use anyhow::{Context, Result};
use std::time::Duration;

async fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
        .await
        .context("HTTP request to user API failed")?;

    let status = response.status();
    if !status.is_success() {
        anyhow::bail!("user API returned status {}", status);
    }

    let data: UserData = response
        .json()
        .await
        .context("failed to parse user data JSON")?;

    Ok(data)
}

async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
    let mut last_error = None;

    for attempt in 1..=attempts {
        match fetch_user_data(user_id).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                last_error = Some(e);
                if attempt < attempts {
                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
                }
            }
        }
    }

    Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}

#[derive(serde::Deserialize)]
struct UserData {
    name: String,
}

Przy łączeniu wielu operacji async należy użyć try_join! z tokio lub futures do uruchamiania ich współbieżnie z propagacją pierwszego błędu.

Downcastowanie i Inspekcja Błędów

Zarówno anyhow::Error jak i Box<dyn Error> wspierają downcastowanie do odzyskania oryginalnego typu błędu. Umożliwia to logowanie konkretnych szczegółów przy jednoczesnej propagacji ogólnych błędów.

downcasting.rsrust
// Inspecting wrapped error types

use anyhow::{Context, Result};
use std::io;

fn log_and_propagate(result: Result<()>) -> Result<()> {
    if let Err(ref e) = result {
        // Check if the root cause is a specific type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            match io_err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::warn!("file not found, using defaults");
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::error!("permission denied - check file ownership");
                }
                _ => {
                    tracing::error!("IO error: {:?}", io_err);
                }
            }
        }
    }
    result
}

Downcastowanie łączy ogólną obsługę błędów ze specyficzną logiką odzyskiwania. Należy używać go oszczędnie—jeśli częste downcastowanie występuje, warto rozważyć czy typowany enum błędu nie byłby lepszy.

Konwersje między Option i Result

Biblioteka standardowa zapewnia metody do konwersji między Option i Result, umożliwiając płynną kompozycję gdy różne API używają różnych wzorców.

conversions.rsrust
// Option and Result interoperability

fn get_env_port() -> Option<u16> {
    std::env::var("PORT")
        .ok()  // Result -> Option (discards error)
        .and_then(|s| s.parse().ok())
}

fn get_env_port_with_error() -> Result<u16, String> {
    std::env::var("PORT")
        .map_err(|_| "PORT not set".to_string())?
        .parse()
        .map_err(|_| "PORT is not a valid number".to_string())
}

fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
    map.get(key)
        .ok_or_else(|| format!("key '{}' not found", key))?  // Option -> Result
        .parse()
        .map_err(|e| format!("parse error for '{}': {}", key, e))
}

Metoda ok() odrzuca szczegóły błędu gdy liczy się tylko obecność. Metody ok_or() i ok_or_else() konwertują None na własny błąd, umożliwiając propagację ? z wartości Option.

Uwagi o Wydajności

Obsługa błędów w Rust nie niesie kosztu w czasie wykonania na ścieżce sukcesu. Result i Option są enumami alokowanymi na stosie o deterministycznym rozmiarze. Kompilator optymalizuje sprawdzenia gdy może udowodnić, że gałąź jest nieosiągalna.

| Podejście | Koszt ścieżki sukcesu | Koszt ścieżki błędu | |----------|------------------|-------------------| | Result/Option | Zero | Odwijanie stosu (tanie) | | panic! | Zero | Pełne odwijanie stosu + czyszczenie | | Wyjątki C++ | Zero (zazwyczaj) | Kosztowna alokacja na stercie + RTTI |

Należy unikać unwrap() i expect() w kodzie bibliotecznym—rezerwując je dla przypadków gdy niepowodzenie naprawdę wskazuje na bug. Dla ścieżek krytycznych wydajnościowo gdzie błędy są częste, warto rozważyć użycie enumów z danymi inline zamiast typów błędów alokowanych na stercie.

Podsumowanie

  • Result<T, E> obsługuje odzyskiwalne niepowodzenia; Option<T> obsługuje brak wartości—oba wymuszają obsługę w czasie kompilacji
  • Operator ? propaguje błędy zwięźle, wymagając implementacji traitu From dla konwersji typów
  • thiserror generuje strukturalne typy błędów dla bibliotek bez boilerplate
  • anyhow zapewnia kontekstowe łańcuchy błędów dla aplikacji, wspierając context(), bail! i ensure!
  • Należy łączyć oba podejścia: biblioteki eksponują typowane błędy przez thiserror, aplikacje opakowują je za pomocą anyhow
  • Kod async używa identycznych wzorców—? działa w blokach async bez modyfikacji
  • Używaj ok_or() do konwersji Option na Result; używaj ok() dla odwrotnej konwersji
  • Downcastuj opakowane błędy tylko gdy specyficzna logika odzyskiwania wymaga oryginalnego typu

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#rust
#error-handling
#best-practices
#thiserror
#anyhow

Udostępnij

Powiązane artykuły