Anyhow Error Handler

Суть

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

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

Особенность применения:

  • Для работы с ошибками в ПРИЛОЖЕНИЯХ (binary)!
  • В библиотеках требуется получать на выходе конкретный тип ошибки, поэтому там применяется thiserror.

Установка

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(())

Замена map_err()

map_err() - это универсальный метод из стандартной библиотеки, который работает с любым Resultcontext() - это метод из anyhow, специально созданный для удобного добавления контекста к ошибкам.

// map_err — нужно явно создавать anyhow::Error
.map_err(|e| anyhow::anyhow!("ошибка: {}", e))

// context — просто добавляет пояснение
.context("ошибка")?

context() принимает замыкание (closure), которое выполняется только при ошибке. Это важно для ресурсоемких операций:

// context с замыканием — форматирование только при ошибке
let content = std::fs::read_to_string(path)
    .with_context(|| format!("не удалось прочитать файл {}", path))?;

// map_err — форматирование всегда, даже при успехе
let content = std::fs::read_to_string(path)
    .map_err(|e| anyhow::anyhow!("не удалось прочитать файл {}", path))?;

with_context() - вариант context() с ленивым вычислением, идеален для дорогих операций вроде форматирования строк.