Правильный запуск нового проекта на Rust с цепочкой ошибок

Перевод | Автор оригинала: Brian Anderson

Пока я готовил последний выпуск error-chain, меня осенило, что не все знают магию error-chain, маленькой библиотеки Rust, которая упрощает правильную обработку ошибок. Обработка ошибок является в Rust первоочередной задачей, она тонко сложна и важна для работоспособности проекта и здравомыслия его сопровождающих, чтобы обработка ошибок выполнялась правильно. С самого начала.

Первое, что я делаю - действительно, самое первое - при запуске любого проекта на Rust - это выбираю стратегию обработки ошибок. А с цепочкой ошибок я заранее точно знаю, что буду делать, и что это будет работать достаточно хорошо. Это дает мне душевное спокойствие.

Итак, этот пост призван продемонстрировать то, что я считаю основным передовым методом настройки обработки ошибок в Rust. Если вы с самого начала будете следовать этим простым инструкциям, у вас будут настроены все ваши леса ошибок, которые будут масштабироваться по мере роста вашего проекта во многих крэйтах, не требуя никаких дополнительных усилий.

Файл quickstart.rs цепочки ошибок демонстрирует простое, но мощное приложение, настроенное с помощью цепочки ошибок. Его можно использовать в качестве шаблона - просто скопируйте этот файл на свой main.rs, и у вас будет приложение, настроенное для надежной обработки ошибок.

Я воспроизведу его здесь полностью:

// Simple and robust error handling with error-chain!
// Use this as a template for new projects.

// `error_chain!` can recurse deeply
#![recursion_limit = "1024"]

// Import the macro. Don't forget to add `error-chain` in your
// `Cargo.toml`!
#[macro_use] extern crate error_chain;

// We'll put our errors in an `errors` module, and other modules in
// this crate will `use errors::*;` to get access to everything
// `error_chain!` creates.
mod errors {
    // Create the Error, ErrorKind, ResultExt, and Result types
    error_chain! { }
}

use errors::*;

fn main() {
    if let Err(ref e) = run() {
        println!("error: {}", e);

        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        // The backtrace is not always generated. Try to run this example
        // with `RUST_BACKTRACE=1`.
        if let Some(backtrace) = e.backtrace() {
            println!("backtrace: {:?}", backtrace);
        }

        ::std::process::exit(1);
    }
}

// Most functions will return the `Result` type, imported from the
// `errors` module. It is a typedef of the standard `Result` type
// for which the error type is always our own `Error`.
fn run() -> Result<()> {
    use std::fs::File;

    // This operation will fail
    File::open("contacts")
        .chain_err(|| "unable to open contacts file")?;

    Ok(())
}

Это говорит само за себя, но я хочу отметить несколько моментов по этому поводу. Во-первых, эта основная функция:

fn main() {
    if let Err(ref e) = run() {
        println!("error: {}", e);

        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        // The backtrace is not always generated. Try to run this example
        // with `RUST_BACKTRACE=1`.
        if let Some(backtrace) = e.backtrace() {
            println!("backtrace: {:?}", backtrace);
        }

        ::std::process::exit(1);
    }
}

Это типично для основных функций, которые я пишу в последнее время. Вся цель состоит в том, чтобы немедленно делегировать функции функции, которая участвует в обработке ошибок (возвращает наши настраиваемые типы результатов и ошибок), а затем обрабатывать эти ошибки. Эта процедура обработки ошибок демонстрирует три части информации, которую цепочка ошибок передает из ошибки: ближайшая ошибка, здесь привязка e; причинная цепочка ошибок, которая привела к этой ошибке; и след исходной ошибки. В зависимости от вашего варианта использования вы можете не беспокоиться об обратных трассировках или можете добавить вызов catch_unwind для борьбы с паниками.

Если вы запустите этот пример, вы увидите следующий результат:

error: unable to open contacts file
caused by: The system cannot find the file specified. (os error 2)

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

fn run() -> Result<()> {
    use std::fs::File;

    // This operation will fail
    File::open("contacts")
        .chain_err(|| "unable to open contacts file")?;

    Ok(())
}

Этот вызов chain_err вызвал новую ошибку с нашим сообщением об ошибке, специфичным для нашего приложения, при сохранении исходной ошибки, сгенерированной File::open, в цепочке ошибок. Это простой пример, но вы можете представить себе, что в более крупных приложениях цепочка ошибок может быть довольно подробной.

Вот как вы начинаете работу с цепочкой ошибок, но это еще не все. Подробнее читайте в документации.

Эффективная цепочка ошибок

Несколько замечаний о том, как я использую цепочку ошибок.

Когда я занимаюсь хакерством, я без колебаний использую строки как ошибки, которые могут быть легко сгенерированы ala bail! («Мне это не нравится: {}», чепуха). Они представлены как ErrorKind::Msg, вариант, определенный для всех типов ошибок, генерируемых цепочкой ошибок! макрос.

Для приложений строки часто подходят как тип ошибки. Однако, когда вы разрабатываете API для публичного использования, именно это становится важным при определении типов ошибок (с помощью блока error_chain! Errors {}). Введенные варианты ошибок дают потребителям вашей библиотеки что-то подходящее. error-chain дает вам возможность делать простые или сложные вещи, она масштабируется в соответствии с потребностями вашего кода.

Ставьте свою error_chain! вызов внутри модуля ошибок и импортировать все содержимое с помощью errors::*. Импорт глобусов - это не то, чем вы хотите много заниматься, но в этом случае закономерность того стоит: вы действительно хотите, чтобы эти четыре типа были под рукой в каждом модуле крэйта.

Я стараюсь не слишком полагаться на автоматическое преобразование из foreign_links {}. Внешние ссылки автоматически преобразуются в локальный тип ошибки. Их легко настроить, и они позволяют легко взаимодействовать с ошибками, не зависящими от вас, но, выполняя автоматическое преобразование, вы теряете возможность вернуть ошибку, более актуальную для вашего приложения. То есть, вместо того, чтобы возвращать ошибку «система не может найти указанный файл», я хочу вернуть ошибку «не удалось открыть файл контактов», вызванную тем, что «система не может найти указанный файл». Каждая ссылка в ошибке поясняет, что пошло не так. Так что вместо того, чтобы использовать? при внешней ошибке используйте chain_err, чтобы дать больше контекста.

error-chain действительно сияет, когда вы начинаете создавать созвездие крэйтов, использующих стратегию цепочки ошибок, и все они связаны друг с другом через error_chain! ссылки {} блоки. Ошибки связанных цепочек ошибок могут распространяться по обратным трассам и иметь структурную форму, которую легко сопоставить, так что, например, вашу ошибку, которая возникла в вашем крэйте утилит, пузырилась через ваш сетевой крэйт, а затем поднималась по крэйту вашего приложения, легко определить с помощью сопоставления с образцом, например:

// Imagine these are crates, not mods
mod utils {
    error_chain! {
        errors { Parse }
    }
}

mod net {
    error_chain! {
        links {
            Utils(::utils::Error, ::utils::ErrorKind);
        }
    }
}

mod app {
    error_chain! {
        links {
            Net(::net::Error, ::net::ErrorKind);
        }
    }

    pub fn run() -> Result<()> {
        match do_something() {
            Err(ErrorKind::Net(::net::ErrorKind::Utils(::utils::ErrorKind::Parse), _)) => {
                ...                                                                                   
            }
            _ => { ... }
        }
    }
}

Такая глубокая диспетчеризация ошибок случается нечасто, но когда это случается, хорошо, что цепочка ошибок упрощает задачу. Опять же, цепочка ошибок позволяет начать с простого, но дает вам возможности по мере роста вашего проекта.

TL;DR

Когда вы начинаете писать новое приложение на Rust, первое, что вы должны спросить, это «как я собираюсь обрабатывать ошибки?»; и ответ, вероятно, должен быть таким: «Я просто собираюсь настроить цепочку ошибок». Настройте цепочку ошибок, используя пример quickstart.rs, и ни о чем не беспокойтесь.

Дополнительные сведения о цепочке ошибок см. В документации.