Обработка ошибок в Rust (Result, ?)

Материал из llmdocs — справочник разработчика
Портал: Go и Rust
Обработка ошибок в Rust (Result, ?)
Восстановимые ошибкиResult — Ok(T) или Err(E)
Отсутствие значенияOption — Some(T) или None
Проброс ошибокоператор ? (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).
  • Добавляйте контекст при пробросе: что делала функция и с какими данными.
  • Не превращайте все ошибки в строки — типизированные ошибки позволяют вызывающему коду реагировать по-разному.

См. также

Источники

  1. The Rust Programming Language — Error Handling
  2. Rust std — Module std::result
  3. Rust std — Trait std::error::Error
  4. Rust Blog — Announcing Rust 1.13
  5. thiserror — docs.rs
  6. anyhow — docs.rs