отсутствие значения и ошибка как часть типаabsence-of-value and error as part of the type

Result, Option и оператор ?Result, Option, and the ? operator

Option<T> выражает возможное отсутствие значения, Result<T, E> — возможный сбой с типизированной ошибкой. Оба — обычные enum'ы без магии компилятора. Оператор ? внутри совместимой функции немедленно распространяет Err/None наружу, разворачивая успешный случай в остальных строках, — сахар над явным match-и-ранним-return на каждом фоллибл-вызове.Option<T> expresses a value that may be absent; Result<T, E> expresses an operation that may fail with a typed error. Both are ordinary enums with no compiler magic. The ? operator, inside a compatible function, immediately propagates an Err/None outward while unwrapping the success case everywhere else — sugar over an explicit match-and-early-return at every fallible call site.

основа языкаcore of the language частый вопрос: как Rust обходится без null и без исключенийcommon question: how Rust does without null and without exceptions

Что этоWhat it is

Option<T> (Some(T) или None) представляет значение, которое может отсутствовать, — альтернатива null-указателям, при этом в безопасном Rust null не существует вовсе. Result<T, E> (Ok(T) или Err(E)) представляет операцию, которая может завершиться типизированной ошибкой. Оба — обычные enum'ы (см. structs-enums.html) без специальной поддержки компилятора, кроме сопоставления с паттерном и большого набора комбинаторных методов (.map, .and_then, .unwrap_or и т.д.). Оператор ?, использованный внутри функции с совместимым возвращаемым типом Result/Option, немедленно распространяет Err/None ранним return'ом, иначе разворачивая успешное значение, — синтаксический сахар, схлопывающий явный match-и-ранний-return на каждом фоллибл-вызове.Option<T> (Some(T) or None) represents a value that may be absent — an alternative to null pointers, with no null at all in safe Rust. Result<T, E> (Ok(T) or Err(E)) represents an operation that may fail with a typed error. Both are ordinary enums (see structs-enums.html) with no special compiler magic beyond pattern-matching support and a large set of combinator methods (.map, .and_then, .unwrap_or, etc.). The ? operator, used inside a function whose return type is compatible, propagates an Err/None immediately by returning early, unwrapping the success value otherwise — syntactic sugar collapsing an explicit match-and-early-return at every fallible call site.

fn divide(a: f64, b: f64) -> Result<f64, DivisionError> {
    if b == 0.0 {
        return Err(DivisionError::ByZero);
    }
    Ok(a / b)
}

Зачем ввелиWhy it was introduced

Null-указатели и исключения — два традиционных источника проблем, которые Option/Result решают на уровне типов:Null pointers and exceptions are two traditional sources of trouble that Option/Result address at the type level:

01

"Ошибка на миллиард долларов" — null как часть типа, а не отдельное невидимое состояниеThe "billion-dollar mistake" — null as part of the type, not a separate invisible state

Тони Хоар, изобретатель null-ссылок, назвал их своей "ошибкой на миллиард долларов" — сбои из-за null (NullPointerException, разыменование null с segfault) остаются одним из самых частых классов рантайм-багов в C/C++/Java и подобных языках. Option<T> делает "это значение может отсутствовать" частью самого типа, проверяемой на этапе компиляции, — использовать Option<T>, как будто это просто T, случайно нельзя: компилятор заставляет явно обработать случай None через match, .unwrap() (явное, видимое утверждение "я уверен, что здесь не будет None") или комбинатор.Tony Hoare, null's inventor, called it his "billion-dollar mistake" — null-related crashes (NullPointerException, null-deref segfaults) remain one of the most common runtime bug classes across C/C++/Java and similar languages. Option<T> makes "this might not have a value" part of the type itself, checked at compile time — you cannot accidentally use an Option<T> as if it were plain T: the compiler forces you to handle the None case somehow, via match, .unwrap() (an explicit, visible admission of "I'm asserting this won't be None"), or a combinator.

02

Исключения невидимы в сигнатуре функцииExceptions are invisible in a function's signature

Традиционный механизм обработки ошибок в C++/Java/Python — исключения — никак не отражён в сигнатуре функции и может быть молча забыт вызывающей стороной. Result<T, E> делает возможность сбоя частью сигнатуры — вызывающий код обязан явно решить, что делать с Err.Exceptions — the traditional error-handling mechanism in C++/Java/Python — are invisible in a function's signature and can be silently forgotten by callers. Result<T, E> makes fallibility part of the signature — the caller must explicitly decide what to do with an Err.

03

Прямой предшественник std::expected в C++23The direct predecessor of C++23's std::expected

Result<T, E> напрямую предвосхищает std::expected в C++23 — сам proposal-документ std::expected прямо ссылается на Rust's Result как источник вдохновения. Идея "ошибка как значение возвращаемого типа, а не как исключение" пришла в C++ спустя годы после того, как Rust построил на ней всю свою модель обработки ошибок.Result<T, E> directly foreshadows C++23's std::expected — std::expected's own proposal paper explicitly cites Rust's Result as its inspiration. The idea of "error as a value in the return type, not an exception" arrived in C++ years after Rust built its entire error-handling model on it.

Как это работает под капотомHow it works under the hood

Без ? распространение ошибки требует явного match на каждом шаге. Оператор ? схлопывает это в одну строку: если значение — Err/None, он немедленно возвращает его из текущей функции; если Ok/Some — разворачивает внутреннее значение и вычисление продолжается.Without ?, propagating an error requires an explicit match at every step. The ? operator collapses that into one line: if the value is Err/None, it immediately returns it from the current function; if Ok/Some, it unwraps the inner value and evaluation continues.

// вручную, через match// manually, via match
fn compute_manual(a: f64, b: f64) -> Result<f64, DivisionError> {
    let value = match divide(a, b) {
        Ok(v) => v,
        Err(e) => return Err(e),
    };
    Ok(value * 2.0)
}

// то же самое, с ?// the same thing, with ?
fn compute(a: f64, b: f64) -> Result<f64, DivisionError> {
    let value = divide(a, b)?;
    Ok(value * 2.0)
}

Комбинаторы Option позволяют работать со значением без раннего return'а: .map преобразует внутреннее значение, если оно есть; .unwrap_or подставляет значение по умолчанию для None; .ok_or превращает Option в Result, снабжая None конкретной ошибкой. .unwrap()/.expect("сообщение") — явные лазейки "запаниковать, если не получилось", противоположные умолчанию "распространяй, а не паникуй" у ?.Option's combinators let you work with the value without an early return: .map transforms the inner value if present; .unwrap_or substitutes a default for None; .ok_or turns an Option into a Result by supplying a concrete error for None. .unwrap()/.expect("message") are the explicit "panic if this fails" escape hatches, the opposite of ?'s "propagate, don't panic" default.

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Парсинг конфигурации с автоматической конвертацией ошибок через FromConfig parsing with automatic error conversion via From

Каждый шаг (чтение файла, парсинг, валидация) может завершиться ошибкой разного исходного типа; общий enum ошибок реализует From для каждого из них, и ? автоматически вызывает From::from на месте, конвертируя конкретную ошибку в единый тип — это то, как одна функция с телом на ? способна распространять ошибки разной природы через единый механизм.Each step (file read, parse, validate) can fail with a different underlying error type; a shared error enum implements From for each of them, and ? automatically calls From::from at the site, converting the specific error into the unified type — this is how a single ?-using function body can propagate errors of different underlying types through one mechanism.

enum ConfigError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    Invalid(String),
}

impl From<std::io::Error> for ConfigError {
    fn from(e: std::io::Error) -> Self { ConfigError::Io(e) }
}
impl From<std::num::ParseIntError> for ConfigError {
    fn from(e: std::num::ParseIntError) -> Self { ConfigError::Parse(e) }
}

fn load_timeout(path: &str) -> Result<u32, ConfigError> {
    let text = std::fs::read_to_string(path)?;   // io::Error -> ConfigError через From// io::Error -> ConfigError via From
    let timeout: u32 = text.trim().parse()?;  // ParseIntError -> ConfigError через From// ParseIntError -> ConfigError via From
    if timeout == 0 {
        return Err(ConfigError::Invalid("timeout must be nonzero".to_string()));
    }
    Ok(timeout)
}
02

Комбинаторы Option вместо вложенных if letOption combinators instead of nested if let

Цепочка .and_then().unwrap_or() выражает "попробовать распарсить, иначе значение по умолчанию" в одной строке — заметно компактнее вложенных if let с тем же смыслом.A .and_then().unwrap_or() chain expresses "try to parse, otherwise a default" in one line — noticeably more compact than nested if let expressing the same thing.

let timeout: u32 = config.get("timeout")
    .and_then(|s| s.parse().ok())
    .unwrap_or(30);

// эквивалент через вложенные if let — заметно многословнее// the nested if let equivalent — noticeably more verbose
let timeout: u32 = if let Some(s) = config.get("timeout") {
    if let Ok(n) = s.parse() {
        n
    } else {
        30
    }
} else {
    30
};
03

.expect() там, где сбой — настоящий баг программиста, а не ожидаемая ситуация.expect() where failure truly is a programmer bug, not an expected situation

Компиляция захардкоженного, заведомо валидного регулярного выражения на старте программы не может провалиться при нормальных обстоятельствах — .expect() здесь не признак лени, а осознанный выбор: паниковать вместо распространения ошибки уместно именно тогда, когда сбой действительно означает баг в самом коде.Compiling a hardcoded, known-valid regex at startup can't realistically fail — .expect() here isn't a sign of laziness, it's a deliberate choice: panicking instead of propagating is appropriate precisely when failure genuinely means a bug in the code itself.

static EMAIL_RE: once_cell::sync::Lazy<regex::Regex> = once_cell::sync::Lazy::new(|| {
    regex::Regex::new(r"^[^@\s]+@[^@\s]+\.[^@\s]+$")
        .expect("hardcoded regex is known to be valid")  // сбой здесь = баг в самой строке regex, не во входных данных// failure here = a bug in the regex literal itself, not in input data
});
04

main(), возвращающий Result<(), Box<dyn Error>>main() returning Result<(), Box<dyn Error>>

Для небольших программ/CLI-утилит, которым не нужны гранулярные кастомные типы ошибок повсюду, распространённый идиоматичный паттерн — дать main возвращать Result<(), Box<dyn Error>>, что позволяет использовать ? вплоть до самого верха программы.For small programs/CLI tools that don't need granular custom error types everywhere, a common idiomatic pattern is to let main return Result<(), Box<dyn Error>>, allowing ? to be used all the way up to the top of the program.

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let timeout = load_timeout("config.toml")?;  // ? работает прямо в main// ? works directly in main
    println!("timeout = {}", timeout);
    Ok(())
}

ГраблиGotchas

? требует совместимый тип возврата у окружающей функции? requires a compatible return type on the enclosing function

? можно использовать только внутри функции, возвращающей совместимый тип (Result с совместимым типом ошибки, либо Option) — использование ? в функции, возвращающей () или несвязанный тип, — ошибка компиляции, иногда неожиданная в замыканиях или в main. Исторический факт: до того, как main разрешили возвращать Result, обычная fn main() вообще не поддерживала ? — требовалась именно сигнатура -> Result<(), E>.? can only be used inside a function whose return type is compatible (a Result with a compatible error type, or an Option) — using ? inside a function returning () or an unrelated type is a compile error, occasionally confusing in closures or main. Historical fact: before main was allowed to return Result, a plain fn main() didn't support ? at all — it specifically required a -> Result<(), E> signature.

.unwrap()/.expect() паникуют — рефлекторное использование это code smell.unwrap()/.expect() panic — reaching for them reflexively is a code smell

.unwrap()/.expect() вызывают немедленную панику (по умолчанию — раскрутку стека и завершение потока, либо abort всего процесса в зависимости от стратегии паники) на None/Err. Рефлекторное обращение к .unwrap() вместо нормальной обработки/распространения ошибки — очень частый антипаттерн в реальном коде, особенно в коде, наспех переведённом с языка с исключениями; в большинстве продакшн-контекстов (за пределами тестов/прототипов/действительно невозможных к провалу случаев) это стоит воспринимать как code smell..unwrap()/.expect() cause an immediate panic (by default, unwinding the stack and terminating the thread, or aborting the whole process depending on the panic strategy configured) on None/Err. Reaching for .unwrap() reflexively instead of proper error handling/propagation is a very common anti-pattern in real Rust code, especially code translated hastily from a language with exceptions, and is worth flagging as a code smell in most production contexts outside of tests/prototypes/genuinely-infallible cases.

Один Result — один конкретный тип ошибки: enum или Box<dyn Error>One Result means exactly one error type: an enum or Box<dyn Error>

Тип ошибки E у Result<T, E> — это ровно один конкретный тип на каждое использование Result — функция, способная провалиться по-настоящему разными путями из разных мест вызова, нуждается либо в едином кастомном enum ошибок (стандартный паттерн, см. пример 1), либо в type-erased Box<dyn Error> (теряется часть возможности сопоставления с конкретной ошибкой ниже по стеку взамен на гибкость) — выбор между "один конкретный enum" и "Box<dyn Error>" — реальное, регулярно встречающееся архитектурное решение в кодовых базах на Rust, о котором стоит думать осознанно, а не выбирать один вариант по умолчанию.Result<T, E>'s error type E is exactly one concrete type per Result usage — a function that can genuinely fail in different ways from different call paths needs either a unified custom error enum (the standard pattern, see example 1) or a type-erased Box<dyn Error> (losing some downstream error-matching specificity in exchange for flexibility) — picking between "one specific enum" and "Box<dyn Error>" is a real, recurring design decision in Rust codebases worth being explicitly aware of rather than defaulting to one without thought.