Обробка Помилок у Rust 2026: 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). Обидва є сум-типами—компілятор гарантує обробку кожного варіанту.
// 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.
// 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 через процедурний макрос.
// 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 додає описові повідомлення до будь-якої помилки.
// 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 при збереженні можливості налагодження.
// 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"),
}
}Цей патерн дозволяє викликаючим зіставляти конкретні варіанти коли відновлення можливе, водночас користуючись багатим контекстом помилки при поширенні невдач вгору. Спільнота Rust значною мірою стандартизувала цей підхід, як обговорюється в рекомендаціях Rust API.
Патерни Обробки Помилок для Асинхронного Коду
Асинхронні функції повертають Result так само як синхронні. Оператор ? працює ідентично всередині async блоків, і як thiserror так і anyhow інтегруються без модифікацій.
// 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 для відновлення оригінального типу помилки. Це дозволяє логувати конкретні деталі при поширенні загальних помилок.
// 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 використовують різні патерни.
// 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: Box, Rc, Arc і RefCell у 2026
Розумні вказівники Rust Box, Rc, Arc і RefCell з поясненнями, компільованими прикладами 2026 року, таблицею вибору та типовими питаннями співбесід.

Rust 2026: Трейти, Дженерики та Просунуті Питання для Співбесід
Повний посібник з трейтів та дженериків у Rust з Edition 2024: trait upcasting, AsyncFn, RPITIT, синтаксис use<> та просунуті питання для технічних співбесід з прикладами коду.

Async/Await у Rust: Tokio, Futures та асинхронна конкурентність
Детальний розбір асинхронного програмування в Rust: від принципів роботи Future до практичних патернів з Tokio, каналів mpsc та обмеження паралелізму через семафори.