resext 0.8.1

A simple, lightweight error handling crate for Rust
Documentation

resext

Main crate providing error handling with context chains

This is the primary interface for ResExt. It re-exports either the proc macro or declarative macro based on feature flags.


Installation

[dependencies]
resext = "0.8"

Quick Example

use resext::resext;

#[resext]
enum FileError {
    Io(std::io::Error),
    Parse(serde_json::Error),
}

fn load_data(path: &str) -> Res<Data> {
    let content = std::fs::read_to_string(path)
        .context("Failed to read file")?;
    
    let data = serde_json::from_str(&content)
        .with_context(|| format!("Failed to parse {}", path))?;
    
    Ok(data)
}

Proc Macro (Default)

The proc macro provides clean syntax with full customization:

#[resext(
    prefix = "ERROR: ",
    suffix = "\n",
    msg_prefix = "  at: ",
    msg_suffix = "",
    msg_delimiter = "\n",
    source_prefix = "Caused by: ",
    include_variant = true,
    alias = MyResult
)]
enum MyError {
    Network(reqwest::Error),
    Database { error: sqlx::Error },
}

Attribute Options

  • prefix - String prepended to entire error message
  • suffix - String appended to entire error message
  • msg_prefix - String prepended to each context message
  • msg_suffix - String appended to each context message
  • msg_delimiter - Separator between context messages | default: " - " (NOTE: the delimiter always includes a newline before it, e.g. if delimiter = " - ", then messages will have "\n - " between them, not just " - ")
  • source_prefix - String prepended to source error (default: "Error: ")
  • include_variant - Include variant name in Display output (default: false)
  • alias - Custom type alias name (default: Res)

Generated Items

The macro generates:

  1. Display and Error implementations for your enum
  2. ResErr struct wrapping your enum with context
  3. ResExt trait with context methods
  4. From implementations for automatic conversion
  5. Type alias for Result<T, ResErr>

Declarative Macro (Zero Dependencies)

For projects that need minimal dependencies:

[dependencies]
resext = { version = "0.8", default-features = false, features = ["declarative"] }
use resext::ResExt;

ResExt! {
    enum MyError {
        Io(std::io::Error),
        Parse(std::num::ParseIntError),
    }
}

Limitations of Declarative Macro

  • No custom formatting options
  • Less ergonomic syntax
  • No named field support
  • Limited customization

Advantages of Declarative Macro

  • Zero dependencies (no syn, quote, proc-macro2)
  • Faster compile times
  • Simpler implementation

Context Methods

.context(msg)

Add static context to an error:

std::fs::read("file.txt")
    .context("Failed to read file")?;

.with_context(|| msg)

Add dynamic context (computed only on error):

std::fs::read(path)
    .with_context(|| format!("Failed to read {}", path))?;

.or_exit(code)

Exit process with given code on error:

let config = load_config().or_exit(1);

.better_expect(|| msg, code)

Like or_exit but with custom message:

let data = load_critical_data()
    .better_expect(|| "FATAL: Cannot start without data", 1);

Error Display Format

Errors are displayed with context chains:

Failed to load application
 - Failed to read config file
 - Failed to open file
Error: No such file or directory

With include_variant = true:

Failed to load application
 - Failed to read config file
Error: Io: No such file or directory

Comparison with Alternatives

vs anyhow

  • ResExt: Type-safe, explicit error enums
  • anyhow: Type-erased, dynamic errors

Use ResExt for libraries (explicit error types), anyhow for applications (flexible error handling).

vs thiserror

  • ResExt: Context chains built-in
  • thiserror: Manual error wrapping

Use ResExt when you need context propagation, thiserror for simple error types.


Examples

Basic Error Handling

#[resext]
enum ConfigError {
    Io(std::io::Error),
    Parse(toml::de::Error),
}

fn load_config(path: &str) -> Res<Config> {
    let content = std::fs::read_to_string(path)
        .context("Failed to read config")?;
    
    toml::from_str(&content)
        .with_context(|| format!("Failed to parse {}", path))
}

Multiple Error Types

#[resext(alias = ApiResult)]
enum ApiError {
    Network(reqwest::Error),
    Database(sqlx::Error),
    Json(serde_json::Error),
}

async fn fetch_user(id: u64) -> ApiResult<User> {
    let response = reqwest::get(format!("/users/{}", id))
        .await
        .context("Failed to fetch user")?;
    
    let user = response.json()
        .await
        .context("Failed to parse user data")?;
    
    Ok(user)
}

Named Fields

#[resext]
enum DatabaseError {
    Connection { error: sqlx::Error },
    Query { error: sqlx::Error },
}

Migration from v0.7.0

Old declarative syntax:

ResExt! {
    enum MyError {
        Io(std::io::Error),
    }
}

New proc macro syntax:

#[resext]
enum MyError {
    Io(std::io::Error),
}

To keep using the declarative macro:

[dependencies]
resext = { version = "0.8", default-features = false, features = ["declarative"] }

Contributing

See CONTRIBUTING.md for guidelines.


License

MIT - See LICENSE for details.