Обработка ошибок в Rust
Ошибка vs исключение — в Rust ожидаемые сбои — Result<T, E>; panic! — когда продолжать выполнение нельзя (как фатальный сбой процесса).
О чём эта статья
В Rust нет исключений в стиле Java: сбой — Result<T, E> или panic! для невосстановимых случаев. match, оператор ?, разница между ожидаемой ошибкой и багом.
Опора: управление, типы.
Интерактивное демо — в Rust нет исключений, ошибки —
Result<T, E>; смотрите сценарий "код ошибки" и стек. Подробнее: ошибки и исключения.
Play ITЗагрузка интерактивного демо…
Основные принципы обработки ошибок в Rust
-
Ошибки — это значения.
Функции, которые могут завершиться неудачей, возвращают типResult<T, E>, где:T— тип успешного результата,E— тип ошибки.
-
Нет
try/catch,throw,raise.
Обработка ошибок происходит явно через сопоставление с образцом (match), макросы (?), или комбинаторы (map,and_thenи т.д.). -
Паника (
panic!) существует, но предназначена только для необратимых ошибок (например, нарушение инвариантов, ошибки в логике программы).- Вызывает разворачивание стека (unwinding) или немедленное завершение (
abort). - Не должна использоваться для обработки ожидаемых ошибок (например, отсутствие файла, недопустимый ввод).
- Вызывает разворачивание стека (unwinding) или немедленное завершение (
Явная обработка через match:
fn parse_port(text: &str) -> Result<u16, String> {
match text.parse::<u16>() {
Ok(port) if port > 0 => Ok(port),
Ok(_) => Err("port must be greater than 0".into()),
Err(_) => Err(format!("invalid port: {text}")),
}
}
Разбор:
text.parse::<u16>()возвращаетResult<u16, ParseIntError>— ожидаемый сбой превращается в значение, а не в панику.Ok(port) if port > 0— успешный разбор с дополнительным условием (guard): ноль отсекается как бизнес-ошибка.Ok(_) => Err(...)перехватывает технически корректный парсинг, но семантически неверное значение.Err(_) => Err(...)преобразует низкоуровневую ошибку парсера в понятное сообщение для вызывающего кода.- Функция возвращает единый тип ошибки
String, скрывая деталиParseIntErrorот внешнего API.
Типы ошибок в Rust
Rust не определяет глобальную иерархию ошибок. Вместо этого используются конкретные типы, реализующие трейт std::error::Error.
1. Стандартные типы ошибок из std
-
std::io::Error— ошибки ввода-вывода:- Возникают при работе с файлами, сетью, процессами.
- Содержит код ошибки (
std::io::ErrorKind), например:NotFoundPermissionDeniedConnectionRefusedInvalidInputTimedOut- и др.
-
std::fmt::Error— ошибка при форматировании (например, в пользовательскомDisplay::fmt). -
std::num::ParseIntError,std::num::ParseFloatError— ошибки при парсинге чисел. -
std::str::Utf8Error— ошибка при проверке UTF-8. -
std::string::FromUtf8Error— ошибка при созданииStringиз байтов. -
std::env::VarError— ошибка при чтении переменной окружения:NotPresentNotUnicode
-
std::ffi::FromBytesWithNulError,std::ffi::NulError— ошибки при работе с C-совместимыми строками. -
std::path::StripPrefixError— ошибка при удалении префикса пути.
2. Трейт std::error::Error
Любой пользовательский тип ошибки должен реализовывать этот трейт (обычно автоматически через thiserror или вручную). Он требует:
Display::fmt— человекочитаемое сообщение,Debug::fmt— техническое представление,- опционально:
source()— ссылка на вложенную ошибку (цепочка причин).
3. Паники (не ошибки в обычном смысле)
Следующие ситуации вызывают panic!:
- Выход за границы массива:
index out of bounds - Деление на ноль
- Нарушение
assert!илиunwrap()наErr/None - Некорректная работа с
unsafe(например, двойное освобождение памяти)
Эти события не являются типами ошибок, а приводят к аварийному завершению потока.
Экосистемные практики
- Библиотеки определяют собственные типы ошибок, часто с помощью крейта
thiserror(для удобного определения) илиanyhow(для упрощённой обработки в приложениях). - Цепочки ошибок строятся через поле
source, а не через наследование. - Нет общего корневого типа ошибок, кроме как через динамическую диспетчеризацию (
Box<dyn std::error::Error>).
Примеры
Option и безопасное извлечение:
fn first_even(numbers: &[i32]) -> Option<i32> {
numbers.iter().copied().find(|n| n % 2 == 0)
}
fn main() {
let values = [1, 3, 4, 7];
match first_even(&values) {
Some(n) => println!("first even: {n}"),
None => println!("no even numbers"),
}
}
Разбор:
findвозвращаетOption<i32>:Someпри первом подходящем элементе,Noneесли чётных нет.copied()превращает итератор ссылок&i32в итератор значенийi32.matchзаставляет обработать оба варианта — компилятор не даст забыть проNone.- Это типичная замена
null: отсутствие значения видно в типе и в ветвлении.
Оператор ? в main:
fn read_port() -> Result<u16, Box<dyn std::error::Error>> {
let raw = std::env::var("APP_PORT")?;
let port: u16 = raw.parse()?;
Ok(port)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let port = read_port()?;
println!("listening on {port}");
Ok(())
}
Разбор:
std::env::var("APP_PORT")?приNotPresentилиNotUnicodeсразу завершаетread_portс ошибкой.raw.parse()?пробрасываетParseIntError, если строка не число.main -> Result<(), ...>позволяет использовать?и на верхнем уровне приложения.- Каждый
?— сокращение дляmatch, который приErrделает ранний return. - В production вместо
println!ошибку обычно логируют и завершают процесс с ненулевым кодом.
use std::fs;
use std::num::ParseIntError;
fn read_number_from_file(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
let contents = fs::read_to_string(path)?; // io::Error
let number: i32 = contents.trim().parse()?; // ParseIntError
Ok(number)
}
Разбор:
use std::fs;подключает модуль работы с файловой системой, здесь используется функция чтения файла целиком в строку.use std::num::ParseIntError;показывает один из конкретных типов ошибок, который может возникнуть при парсинге числа.- Сигнатура
Result<i32, Box<dyn std::error::Error>>говорит, что функция либо возвращает числоi32, либо любую ошибку, реализующую трейтError. fs::read_to_string(path)?читает файл по путиpath; оператор?автоматически завершает функцию с ошибкой, если чтение не удалось.contents.trim().parse()?удаляет пробелы/переносы по краям и пытается преобразовать строку вi32; ошибки парсинга также пробрасываются через?.Ok(number)формирует успешный результат и явно показывает "счастливый путь" выполнения функции.- Такой пример демонстрирует главный стиль Rust: ошибки не прячутся, а проходят по типам и обрабатываются контролируемо.
Здесь возможны две конкретные ошибки: std::io::Error и ParseIntError. Обе реализуют std::error::Error и могут быть упакованы в Box<dyn Error>.
Нет типов ошибок
В Rust нет единого списка "типов ошибок", аналогичного другим языкам, потому что:
- Ошибки представлены конкретными типами, а не иерархией классов.
- Нет встроенных исключений вроде
IndexError,KeyError. - Стандартная библиотека предоставляет набор специализированных типов ошибок для разных подсистем.
- Пользовательские ошибки определяются явно и композиционно.
Таким образом, вместо таксономии исключений Rust предлагает типобезопасную, композиционную модель обработки ошибок через Result и трейт Error.