Обработка ошибок в Rust (Result, ?)
| Восстановимые ошибки | Result |
|---|---|
| Отсутствие значения | Option |
| Проброс ошибок | оператор ? (Rust 1.13, 2016; ранее макрос try!) |
| Неустранимые ошибки | panic! |
| Трейт ошибок | std::error::Error |
| Популярные крейты | thiserror (библиотеки), anyhow (приложения) |
Обработка ошибок в Rust основана на типах. Операция, которая может завершиться неудачей, возвращает Result<T, E> — перечисление из двух вариантов: Ok(значение) при успехе или Err(ошибка) при неудаче. Значение, которого может не быть, описывается Option<T> — Some(значение) или None. Компилятор не позволит использовать результат, не разобрав оба варианта, а оператор ? позволяет коротко передать ошибку наверх. Для действительно неустранимых ситуаций есть panic!.[1]
В Rust нет исключений и нет null: всё, что может пойти не так, видно в сигнатуре функции.
Result и Option
enum Result<T, E> { Ok(T), Err(E) }
enum Option<T> { Some(T), None }
use std::fs;
fn read_port(path: &str) -> Result<u16, String> {
let text = match fs::read_to_string(path) {
Ok(t) => t,
Err(e) => return Err(format!("не удалось прочитать {path}: {e}")),
};
text.trim().parse::<u16>().map_err(|e| format!("некорректный порт: {e}"))
}
match read_port("port.txt") {
Ok(port) => println!("слушаем порт {port}"),
Err(msg) => eprintln!("ошибка: {msg}"),
}
У обоих типов десятки удобных методов-комбинаторов: map, map_err, and_then, unwrap_or, unwrap_or_else, ok_or, ok(). Они позволяют преобразовывать значения и ошибки без вложенных match.[2]
Оператор ?
Оператор ? после выражения типа Result делает следующее: при Ok извлекает значение, при Err немедленно возвращает ошибку из текущей функции, предварительно преобразовав её в нужный тип через трейт From. Он появился в Rust 1.13 (2016), заменив макрос try!.[4]
use std::{fs, io, num::ParseIntError};
#[derive(Debug)]
enum ConfigError {
Io(io::Error),
Parse(ParseIntError),
}
impl From<io::Error> for ConfigError { fn from(e: io::Error) -> Self { ConfigError::Io(e) } }
impl From<ParseIntError> for ConfigError { fn from(e: ParseIntError) -> Self { ConfigError::Parse(e) } }
fn read_port(path: &str) -> Result<u16, ConfigError> {
let text = fs::read_to_string(path)?; // io::Error → ConfigError::Io
let port = text.trim().parse::<u16>()?; // ParseIntError → ConfigError::Parse
Ok(port)
}
Тот же оператор работает с Option в функциях, возвращающих Option, и в функции main, если она возвращает Result.
panic!
panic! прерывает поток выполнения при ошибке программиста или нарушенном инварианте: выход за границы вектора, деление на ноль в целых числах, явный unreachable!(). Методы unwrap() и expect("сообщение") извлекают значение из Result или Option, вызывая панику при ошибке. Они уместны в прототипах, тестах и там, где ошибка действительно невозможна; в рабочем коде предпочтительнее обработка или expect с объяснением, почему значение обязано быть.
Трейт Error и экосистема
Типы ошибок принято реализовывать с трейтом std::error::Error (требует Debug и Display) и методом source(), который даёт доступ к исходной причине.[3] Писать такие реализации вручную утомительно, поэтому сложились два популярных крейта Дэвида Толнея:
| Крейт | Для чего | Подход |
|---|---|---|
| thiserror | библиотеки | собственное перечисление ошибок; derive генерирует Display, Error и From[5] |
| anyhow | приложения | единый тип anyhow::Error для любых ошибок, удобное добавление контекста[6] |
// библиотека: точные, различимые ошибки
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("запись {0} не найдена")]
NotFound(u64),
#[error("ошибка базы данных")]
Db(#[from] sqlx::Error),
}
// приложение: ошибки с контекстом
use anyhow::{Context, Result};
fn load() -> Result<Config> {
let raw = std::fs::read_to_string("config.toml").context("чтение config.toml")?;
let cfg: Config = toml::from_str(&raw).context("разбор config.toml")?;
Ok(cfg)
}
Правило большого пальца: библиотека должна давать вызывающему коду возможность различать ошибки (thiserror), а приложению чаще важно понятное сообщение с цепочкой причин (anyhow).
Сравнение с Go и исключениями
| Rust | Go | Исключения (Python, Java) | |
|---|---|---|---|
| Где видна ошибка | в типе возврата | в типе возврата | не видна в сигнатуре (кроме checked exceptions) |
| Можно забыть обработать | нет — компилятор предупредит | да, если проигнорировать err |
да |
| Проброс | ? |
if err != nil { return err } |
автоматический |
| Стоимость | нулевая, обычное значение | обычное значение | раскрутка стека |
Советы
- Не злоупотребляйте
unwrap()— каждая паника в продакшене начинается с одного из них; линтер Clippy может запрещать их в рабочем коде (см. Clippy и rustfmt). - Добавляйте контекст при пробросе: что делала функция и с какими данными.
- Не превращайте все ошибки в строки — типизированные ошибки позволяют вызывающему коду реагировать по-разному.