Обробка Помилок у Rust 2026: Result, Option, thiserror та anyhow

Повний посібник з обробки помилок у Rust з використанням Result, Option, оператора ? та бібліотек thiserror і anyhow. Найкращі практики та патерни для продакшн застосунків.

Обробка помилок у Rust з Result, Option, thiserror та anyhow

Обробка помилок у Rust базується на двох перерахуваннях—Result<T, E> та Option<T>—які змушують явно обробляти успіх, невдачу та відсутність значення на етапі компіляції. На відміну від винятків, що поширюються непомітно, підхід Rust робить шляхи помилок видимими у сигнатурах функцій, усуваючи цілі категорії несподіванок під час виконання. Оператор ? у поєднанні з бібліотеками на кшталт thiserror та anyhow спрощує цю явну модель без втрати зрозумілості.

Коли використовувати який тип

Використовуйте Option<T> для значень, які можуть законно бути відсутніми (поля конфігурації, результати пошуку). Використовуйте Result<T, E> коли операції можуть завершитися невдачею зі значущою інформацією про помилку (файлові операції, мережеві запити, парсинг).

Основи Result та Option

Result<T, E> представляє успіх (Ok(T)) або невдачу (Err(E)). Option<T> представляє наявність значення (Some(T)) або його відсутність (None). Обидва є сум-типами—компілятор гарантує обробку кожного варіанту.

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),
    }
}

Компілятор відхиляє код, що ігнорує ці повернуті значення без явної обробки. Такий дизайн виявляє помилки на етапі компіляції, які в інших мовах проявлялися б як виключення null pointer або неперехоплені помилки.

Оператор Знак Питання для Лаконічного Поширення

Оператор ? перетворює громіздкі ланцюжки match у читабельний лінійний код. При застосуванні до Result він повертає помилку достроково якщо вона присутня, або розгортає значення успіху. Те саме стосується 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)
}

Оператор ? вимагає, щоб тип помилки був конвертованим до типу помилки, що повертається функцією, через трейт From. Використання Box<dyn std::error::Error> як показано вище приймає будь-який тип помилки, що реалізує стандартний трейт Error.

Створення Власних Помилок з thiserror

Бібліотека thiserror усуває шаблонний код для власних типів помилок. Вона виводить реалізації Error, Display та From через процедурний макрос.

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,
}

Атрибут #[from] генерує автоматичні конверсії, забезпечуючи безперешкодне використання ? з різними типами вихідних помилок. Повідомлення про помилки стають самодокументованими завдяки форматним рядкам #[error(...)].

Помилки Рівня Застосунку з anyhow

Поки thiserror підходить для бібліотечного коду зі специфічними типами помилок, anyhow націлений на застосунки, де контекст помилки важливіший за гранулярність типу. Трейт Context додає описові повідомлення до будь-якої помилки.

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 }
}

Метод context() обгортає помилки додатковою інформацією, створюючи ланцюжок, що допомагає при налагодженні. Макрос bail! забезпечує достроковий вихід з форматованим повідомленням про помилку, тоді як ensure! діє як перевірка, що повертає помилку замість паніки.

Готовий до співбесід з Rust?

Практикуйся з нашими інтерактивними симуляторами, flashcards та технічними тестами.

Поєднання thiserror та anyhow у Реальних Проектах

Бібліотеки надають структуровані помилки через thiserror для програмної обробки споживачами. Застосунки обгортають ці помилки за допомогою anyhow для людино-читабельного виводу. Такий поділ зберігає чистоту API при збереженні можливості налагодження.

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"),
    }
}

Цей патерн дозволяє викликаючим зіставляти конкретні варіанти коли відновлення можливе, водночас користуючись багатим контекстом помилки при поширенні невдач вгору. Спільнота Rust значною мірою стандартизувала цей підхід, як обговорюється в рекомендаціях Rust API.

Патерни Обробки Помилок для Асинхронного Коду

Асинхронні функції повертають Result так само як синхронні. Оператор ? працює ідентично всередині async блоків, і як thiserror так і anyhow інтегруються без модифікацій.

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,
}

При комбінуванні кількох async операцій слід використовувати try_join! з tokio або futures для їх паралельного виконання з поширенням першої помилки.

Downcast та Інспекція Помилок

Як anyhow::Error так і Box<dyn Error> підтримують downcast для відновлення оригінального типу помилки. Це дозволяє логувати конкретні деталі при поширенні загальних помилок.

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
}

Downcast з'єднує загальну обробку помилок зі специфічною логікою відновлення. Його слід використовувати обережно—якщо частий downcast відбувається, варто розглянути чи не буде типізований enum помилок кращим рішенням.

Конвертація між Option та Result

Стандартна бібліотека надає методи для конвертації між Option та Result, забезпечуючи плавну композицію коли різні API використовують різні патерни.

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))
}

Метод ok() відкидає деталі помилки коли важлива лише наявність. Методи ok_or() та ok_or_else() конвертують None у власну помилку, дозволяючи поширення ? зі значень Option.

Зауваження щодо Продуктивності

Обробка помилок у Rust не несе витрат під час виконання на шляху успіху. Result та Option є перерахуваннями, що алокуються на стеку з детермінованим розміром. Компілятор оптимізує перевірки коли може довести, що гілка недосяжна.

| Підхід | Вартість шляху успіху | Вартість шляху помилки | |----------|------------------|-------------------| | Result/Option | Нуль | Розкручування стеку (дешево) | | panic! | Нуль | Повне розкручування стеку + очищення | | Винятки C++ | Нуль (зазвичай) | Дорога алокація на купі + RTTI |

Уникайте unwrap() та expect() у бібліотечному коді—резервуйте їх для випадків де невдача справді вказує на баг. Для критичних до продуктивності шляхів де помилки часті, розгляньте використання enum з вбудованими даними замість типів помилок з алокацією на купі.

Висновок

  • Result<T, E> обробляє відновлювані невдачі; Option<T> обробляє відсутність—обидва забезпечують обробку на етапі компіляції
  • Оператор ? поширює помилки лаконічно, вимагаючи реалізацій трейту From для конвертації типів
  • thiserror генерує структуровані типи помилок для бібліотек без шаблонного коду
  • anyhow забезпечує контекстні ланцюжки помилок для застосунків, підтримуючи context(), bail! та ensure!
  • Поєднуйте обидва: бібліотеки надають типізовані помилки через thiserror, застосунки обгортають їх за допомогою anyhow
  • Асинхронний код використовує ідентичні патерни—? працює в async блоках без модифікацій
  • Використовуйте ok_or() для конвертації Option у Result; використовуйте ok() для зворотної конвертації
  • Робіть downcast обгорнутих помилок лише коли специфічна логіка відновлення вимагає оригінального типу

Починай практикувати!

Перевір свої знання з нашими симуляторами співбесід та технічними тестами.

Теги

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

Поділитися

Пов'язані статті