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 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.
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.
// 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.
// 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.
// 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.
// 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.
// 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;// 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.
// 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.
// 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.
// 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 traituFromdla konwersji typów thiserrorgeneruje strukturalne typy błędów dla bibliotek bez boilerplateanyhowzapewnia kontekstowe łańcuchy błędów dla aplikacji, wspierająccontext(),bail!iensure!- 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 konwersjiOptionnaResult; używajok()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
Udostępnij
Powiązane artykuły

Inteligentne wskaźniki w Rust: Box, Rc, Arc i RefCell w 2026
Inteligentne wskaźniki Rust Box, Rc, Arc i RefCell wyjaśnione na kompilowalnych przykładach z 2026, z tabelą decyzyjną i pytaniami rekrutacyjnymi.

Rust 2026: Traity, Generyki i Zaawansowane Pytania Rekrutacyjne
Kompletny przewodnik po traitach i generykach w Rust z edycji 2024: trait upcasting, AsyncFn, RPITIT, składnia use<> oraz zaawansowane pytania rekrutacyjne z przykładami kodu.

Async/Await w Rust: Tokio, Futures i asynchroniczna współbieżność w praktyce
Kompletny przewodnik po programowaniu asynchronicznym w Rust. Artykuł wyjaśnia mechanizm futures, runtime Tokio, spawning zadań, kanały mpsc, obsługę błędów oraz wzorce produkcyjne z ograniczaniem współbieżności.