# Обработка ошибок в Rust: паника, Option, Result и как с ними дружить

> Простой и понятный разбор обработки ошибок в Rust. Сравниваем подходы в разных языках, разбираемся с Option и Result, оператором `?` и учимся писать свои ошибки.

- **URL:** https://eagle2000.space/rust/rust-errors-handling/
- **Date:** 2026-08-02

- **Tags:** rust, dev, errors

---## Введение

Ошибки в коде случаются у всех. Вопрос лишь в том, как язык программирования помогает нам с ними справляться. За годы развития IT-индустрии разработчики перепробовали много разных подходов: от простых возвращаемых циферок-кодов до сложных и тяжелых исключений. 

Rust пошел своим путем. Вместо скрытых падений и неявного поведения он предлагает нам два четких инструмента: **невосстановимые ошибки** (когда продолжать работу бессмысленно) и **восстановимые ошибки** через специальные типы-контейнеры `Option<T>` и `Result<T, E>`.

Давайте разберем, как работают ошибки в разных языках, научимся пользоваться методами стандартной библиотеки Rust и разберемся, зачем нужны сторонние библиотеки вроде `thiserror` и `anyhow`.

---

## 1. Как с ошибками борются в других языках?

### Язык C: Коды ошибок

В C функции обычно возвращают числовой код ошибки (например, `-1` при сбое и `0` при успехе), а полезные данные передаются через указатели в аргументах.

```c
int get_data(input in, output *out) {
    if (something_wrong) {
        return -1; // Ошибка
    }
    return 0; // Все ок
}
```

* **В чем проблема:**
  1. Разработчик может легко проигнорировать возвращаемый код и работать дальше с пустыми данными.
  2. Успешный результат и ошибка смешиваются в рамках одного и того же типа `int`.

### C++, Java, C#, Python, JS: Исключения (Exceptions)

В этих языках для сообщения об ошибке используют конструкцию `throw` / `raise`, которая прерывает выполнение кода и летит вверх, пока кто-то не поймает её в блок `catch` (или `except`).

```cpp
void process() {
    try {
        do_work();
    } catch (const std::exception& e) {
        // Ловим ошибку здесь
    }
}
```

* **В чем проблема:**
  1. Ошибку все еще легко случайно проигнорировать, написав пустой блок `catch`.
  2. Исключения сильно замедляют программу в момент их выброса (оверхед на раскрутку стека).
  3. Сигнатура функции не говорит о том, какие именно исключения она может выбросить. Код становится непредсказуемым.

### Язык Go: Возврат пары значений

В Go функции возвращают кортеж (результат, ошибка).

```go
res, err := doSomething()
if err != nil {
    return nil, err
}
```

* **В чем проблема:**
  1. Ошибку легко заглушить, присвоив ее символу `_`.
  2. Приходится писать огромное количество шаблонного кода `if err != nil` на каждом шагу.

### Haskell, Scala, TypeScript

Используют так называемые алгебраические типы данных (ADT) и обертки для значений (Option/Either). Именно эта идея легла в основу Rust, позволив сделать обработку ошибок безопасной и красивой.

---

## 2. Невосстановимые ошибки: Паника (`panic!`)

Иногда в программе происходит то, к чему мы вообще не готовы: например, выход за границы массива или деление на ноль. В Rust такие ситуации приводят к **панике**.

* **Что такое паника:** Это мгновенная остановка выполнения текущего потока.
* **Главное правило:** Программа не должна пытаться обрабатывать панику в обычном коде. Лучшее решение при панике — упасть, чтобы не испортить данные в базе данных или файлы на диске.
* **Как вызвать:** С помощью макроса `panic!("Сообщение об ошибке")` или методов извлечения значения `.unwrap()` / `.expect()`, если они вызваны на пустом значении.

---

## 3. Восстановимые ошибки: Обертки Option и Result

Если ошибка ожидаема (например, файл не найден или пользователь ввел буквы вместо числа), мы используем типы-контейнеры.

### Тип `Option<T>`

Используется, когда значение может просто отсутствовать.

```rust
pub enum Option<T> {
    None,    // Значения нет
    Some(T), // Значение есть и внутри лежит тип T
}
```

Благодаря этому, компилятор заставит вас обработать случай `None` перед тем, как вы сможете получить доступ к данным внутри `Some`.

### Тип `Result<T, E>`

Используется, когда операция может завершиться ошибкой, и нам важно знать её причину.

```rust
pub enum Result<T, E> {
    Ok(T),  // Успех, внутри лежит результат типа T
    Err(E), // Сбой, внутри лежит ошибка типа E
}
```

Тип `Result` помечен специальным атрибутом `#[must_use]`. Если функция возвращает `Result`, а вы проигнорируете его (не сохраните в переменную и не обработаете), компилятор выдаст предупреждение.

---

## 4. Шпаргалка по методам Option и Result

Для работы со значениями внутри `Option` и `Result` стандартная библиотека предоставляет множество удобных методов.

| Группа методов | Для `Option<T>` | Для `Result<T, E>` | Что делает / Нюансы |
| --- | --- | --- | --- |
| **Жёсткое извлечение (Паника при сбое)** | `unwrap()` <br> `expect(&str)` | `unwrap()` <br> `expect(&str)` <br> `unwrap_err()` | Достают значение. Если внутри `None`/`Err` — программа **падает**. В `expect` можно передать сообщение для лога. `unwrap_err` достает ошибку, паникуя при успехе. |
| **Безопасное извлечение по умолчанию** | `unwrap_or(default)` <br> `unwrap_or_else(f)` <br> `unwrap_or_default()` | `unwrap_or(default)` <br> `unwrap_or_else(f)` <br> `unwrap_or_default()` | Возвращают значение или дефолтный вариант. В `_or_else` дефолт вычисляется лениво через функцию `f`. |
| **Простые проверки** | `is_some()` <br> `is_none()` | `is_ok()` <br> `is_err()` | Возвращают `true` или `false`. Не забирают владение значением. |
| **Трансформация содержимого** | `map(f)` | `map(f)` <br> `map_err(f)` | Применяют функцию `f` к значению внутри, если оно есть. Противоположное состояние не трогают. `map_err` меняет только тип ошибки. |
| **Логические цепочки (И)** | `and(opt)` <br> `and_then(f)` | `and(res)` <br> `and_then(f)` | `and_then` (часто называют flat_map) принимает функцию, которая возвращает новый `Option` или `Result`. Позволяет выстраивать цепочки без вложенности. |
| **Логические цепочки (ИЛИ)** | `or(opt)` <br> `or_else(f)` | `or(res)` <br> `or_else(f)` | Возвращают оригинал, если он успешный. Если там `None`/`Err` — возвращают альтернативу. |
| **Преобразования типов** | `transpose()` <br> `ok_or(err)` <br> `ok_or_else(f)` | `transpose()` <br> `ok()` <br> `err()` | `transpose` меняет вложенность: `Option<Result>` превращает в `Result<Option>`. `ok_or` превращает `Option` в `Result`, заменяя `None` на указанную ошибку. `ok()` и `err()` делают из `Result` опциональные значения. |

---

## 5. Лаконичный проброс ошибок: Оператор `?`

Чтобы не писать постоянно конструкции `match` или `if let`, в Rust есть оператор `?`. Он ставится в конце выражения, возвращающего `Option` или `Result`.

```rust
let parsed = val_str.parse::<i32>()?;
```

**Как он работает:**
* Если внутри `Result` лежит `Ok`, оператор `?` просто достает чистое значение и код идет дальше.
* Если внутри лежит `Err`, выполнение функции немедленно прекращается, и эта ошибка возвращается наверх в вызывающий код.
* **Магия приведения типов:** Под капотом компилятор автоматически вызывает метод `From::from(error)`. Это значит, что если ваша функция возвращает большую общую ошибку, а локальная функция вернула маленькую специфичную ошибку, Rust сам превратит её в нужный тип, если для них реализована конвертация.

---

## 6. Создание своих типов ошибок

В реальных проектах нам часто нужно детально описывать свои ошибки.

### Стандартный трейт `std::error::Error`

Любой наш тип ошибки должен реализовывать трейты форматирования:
1. `Debug` — технический вывод для логов (выводится через `{:?}`, легко сгенерировать с помощью `#[derive(Debug)]`).
2. `Display` — красивый текст для людей (выводится через `{}`).

### Библиотеки-помощники

Чтобы не писать тонны рутинного кода для вывода ошибок, разработчики используют готовые крейты:

* **`anyhow` (для приложений):**
  Отлично подходит для конечных программ (application-level). Позволяет свести все возможные ошибки в проекте к одному типу `anyhow::Error` и не расписывать их вручную.
* **`thiserror` (для библиотек):**
  Идеально подходит для библиотек и переиспользуемых модулей. Автоматически создает красивый вывод ошибок (`Display`) и приведения типов на основе простых аннотаций прямо над перечислением.
* **`snafu`:**
  Еще один мощный инструмент для гибкой настройки ошибок с сохранением контекста.

Давайте посмотрим, как работать с Option, Result, оператором `?` и библиотекой `thiserror` на интерактивных примерах кода:


### 💻 Обработка ошибок в Rust


#### Шаг 1

```rust
fn main() {
    let raw_input: Option<&str> = Some("42");
    
    // Преобразуем строку в число и умножаем на 2
    let processed = raw_input
        .and_then(|s| s.parse::<i32>().ok())
        .map(|num| num * 2);
        
    println!("Обработанное значение: {:?}", processed);
}
```

**Комбинаторы Option и Result**
- Вместо матчинга можно лаконично преобразовывать данные с помощью `map` и `and_then`.
- Метод `map` применяет функцию к значению внутри `Some` или `Ok`.
- Метод `and_then` принимает замыкание, возвращающее новый `Option`/`Result` — удобно для цепочки зависимых операций.


#### Шаг 2

```rust
use std::num::ParseIntError;

fn parse_and_double(val_str: &str) -> Result<i32, ParseIntError> {
    // Если parse вернет Err, выполнение прервется и вернется Err
    let parsed = val_str.parse::<i32>()?;
    Ok(parsed * 2)
}
fn main() {
    match parse_and_double("21") {
        Ok(res) => println!("Успех: {}", res),
        Err(e) => println!("Ошибка разбора: {}", e),
    }
    
    if parse_and_double("invalid").is_err() {
        println!("Функция корректно вернула ошибку при невалидном вводе.");
    }
}
```

**Оператор проброса ошибок `?`**
- Оператор `?` разворачивает `Ok(T)` или немедленно возвращает `Err(E)` из текущей функции.
- Он также автоматически приводит типы ошибок с помощью типажа `From` (`From::from`).
- Это позволяет прозрачно трансформировать локальные ошибки в общую ошибку функции.


#### Шаг 3

```rust
use thiserror::Error;

#[derive(Error, Debug)]
enum DataError {
    #[error("Ошибка ввода-вывода при обращении к файлу: {0}")]
    Disconnect(#[from] std::io::Error),
    
    #[error("Некорректный формат данных в строке: {0}")]
    InvalidFormat(String),
}

fn read_data() -> Result<String, DataError> {
    // Симулируем чтение с ошибкой формата
    Err(DataError::InvalidFormat("Не хватает заголовка".to_string()))
}
fn main() {
    match read_data() {
        Ok(data) => println!("Данные: {}", data),
        Err(e) => println!("Произошла ошибка: {}", e),
    }
}
```

**Кастомные ошибки с thiserror**
- Крейт `thiserror` генерирует реализации трейта `Error` и форматирования `Display`.
- Мы аннотируем варианты макросом `#[error(...)]` для пользовательского вывода.
- Атрибут `#[from]` автоматически реализует типаж `From` для конвертации ошибок.


---

## Проверь свои знания!

Пройдите короткий интерактивный тест, чтобы закрепить понимание обработки ошибок в Rust.


### 📝 Обработка ошибок в Rust


**1. Какое поведение демонстрирует макрос `panic!` в Rust?**

- [ ] Возвращает специальный код ошибки для обработки в вызывающей функции
- [ ] Приводит к фатальному завершению выполнения текущего потока (панике) без возможности стандартного восстановления
- [ ] Приостанавливает выполнение программы и ожидает ввода пользователя

**2. В чем главное отличие метода `.expect(&str)` от `.unwrap()`?**

- [ ] `.expect()` не паникует при ошибке
- [ ] `.expect()` позволяет передать кастомное сообщение, которое будет выведено в консоль при возникновении паники
- [ ] `.expect()` работает только с `Result`, но не с `Option`

**3. Какую важную операцию под капотом выполняет оператор `?` при работе с `Result`?**

- [ ] Всегда прерывает программу и вызывает `panic!`
- [ ] Логирует ошибку в stderr перед возвратом
- [ ] Автоматически преобразует локальный тип ошибки в тип ошибки возвращаемого значения функции с помощью трейта `From`

**4. Для чего предпочтительно использовать крейт `anyhow` по сравнению с `thiserror`?**

- [ ] В коде библиотек, где важна строгая дифференциация ошибок
- [ ] В коде приложений (application-level), где нужно быстро привести все разнородные ошибки к общему типу `anyhow::Error`
- [ ] Только для написания юнит-тестов

