Anyhow Error Handler

Библиотека работы с ошибками - https://docs.rs/anyhow/latest/anyhow/

  • Использует единый тип Error для всех ошибок;
  • Добавляет контекст к ошибкам;
  • Поддержка backtrace для debug;

Установка

cargo add anyhow

Использование

Anyhow создаёт алиас наподобие Result<T> = Result<T, Box<dyn Error>>, чтобы скрыть тип ошибок и сделать его универсальным.

// ---- Без Anyhow
fn string_error() -> Result<(), String> {
    Ok(())
}
fn io_error() -> Result<(), std::io::Error> {
    Ok(())
}
fn any_error() -> Result<(), Box<dyn Error>> {
    string_error()?;
    io_error()?;
    Ok(())
}
// ---- С Anyhow:
use anyhow::Result;

fn string_error() -> Result<()> {
    Ok(())
}
fn io_error() -> Result<()> {
    Ok(())
}
fn any_error() -> Result<()> {
    string_error()?;
    io_error()?;
    Ok(())
}

Пример неудачного чтения файла:

use anyhow::{Context, Result};

fn read_config_file(path: &str) -> Result<String> {
    std::fs::read_to_string(path).with_context(|| format!("Failed to read file {}", path))
}

fn main() -> Result<()> {
    let config_content = read_config_file("conf.txt")?;

    println!("Config content:\n{:?}", config_content);

    Ok(())
}
  • Result<T> становится алиасом к Result<T, anyhow::Error>;
  • Context, with_context() позволяет добавить подробности к ошибке, в случае неуспеха функции чтения read_to_string();
  • Оператор ? выносит ошибку вверх, при этом авто-конвертирует её тип в anyhow::Error.

Замена Box<dyn> с контекстом

Возьмём пример, в котором чтение файла std::fs::read_to_string() (может быть неудачным), далее дешифровка его контента с помощью base64 decode() (может не получиться) в цепочку байт, из которой формируется строка String::from_utf8() (может не получиться). Все эти три потенциальных ошибки имеют разный тип.

Один способ все три их принять на одну функцию, это с помощью Box<dyn std::error::Error>>, потому что все 3 ошибки применяют std::error::Error.

use base64::{self, engine, Engine};

fn decode() -> Result<(), Box<dyn std::error::Error>> {
    let input = std::fs::read_to_string("input")?;
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD.decode(line)?;
        println!("{}", String::from_utf8(bytes)?);
    }
    Ok(())
}

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

В случае применения Anyhow, можно заменить им Box<dyn>, при этом сразу добавить контекстные сообщения, которое поможет понять место:

use anyhow::Context;
use base64::{self, engine, Engine};

fn decode() -> Result<(), anyhow::Error> {
    let input = std::fs::read_to_string("input")
    .context("Failed to read file")?; // контекст 1
    for line in input.lines() {
        let bytes = engine::general_purpose::STANDARD
            .decode(line)
            .context("Failed to decode the input")?; // контекст 2
        println!(
            "{}",
            String::from_utf8(bytes)
            .context("Failed to cenvert bytes")? // контекст 3
        );
    }
    Ok(())