Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

hooq is an attribute macro that can (when desired) insert methods in front of the ? operator1, return, and tail expressions.

use hooq::hooq;

#[hooq]
#[hooq::method(.map(|v| v * 2))]
fn double(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
    let res = s.parse::<u32>()?;
    Ok(res)
}

fn double_expanded(s: &str) -> Result<u32, Box<dyn std::error::Error>> {
    let res = s.parse::<u32>().map(|v| v * 2)?;
    Ok(res)
}

#[test]
fn test() {
    assert_eq!(double("21").unwrap(), double_expanded("21").unwrap());
}

fn main() {
    println!("double_hooked: {}", double("21").unwrap());
    println!("double: {}", double_expanded("21").unwrap());
}

It ships with rich presets so you can easily hook error logging and similar behavior onto Result values.

Why use hooq?

We will motivate hooq using the following source code containing a function that returns a Result type:

use std::error::Error;

fn load_host_and_port() -> Result<String, Box<dyn Error>> {
    // Loading APP_HOST
    let host = std::env::var("APP_HOST")?;

    // Loading APP_PORT
    let port = std::env::var("APP_PORT")?;

    // Convert to u16
    let port: u16 = port.parse()?;

    Ok(format!("{host}:{port}"))
}

fn main() -> Result<(), Box<dyn Error>> {
    let host_and_port = load_host_and_port()?;

    println!("Server is running on: {}", host_and_port);

    // snip

    Ok(())
}

#[test]
fn test_load_host_and_port() {
    unsafe {
        std::env::set_var("APP_HOST", "localhost");
        std::env::set_var("APP_PORT", "8080");
    }
    let host_and_port = load_host_and_port().unwrap();
    assert_eq!(host_and_port, "localhost:8080");
}

If you run it with all required environment variables set, it works without errors:

$ APP_HOST=127.0.0.1 APP_PORT=10 cargo run -q
Server is running on: 127.0.0.1:10

Now run it in a way that triggers an error:

$ APP_PORT=10 cargo run -q
Error: NotPresent

The contents of the Box<dyn Error> returned from main are printed; we (probably) have a missing environment variable.

But this error output is terrible:

  • You cannot tell the context or what kind of error it is
  • You cannot tell where the error occurred

Most likely the author of this Rust program did not want to build a formal application with fine‑grained error handling, but just a small casual CLI tool. Rust’s error output, however, can be rather unforgiving if you cut corners2.

Enter the hooq attribute macro!

use std::error::Error;

use hooq::hooq;

#[hooq]
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
    // Loading APP_HOST
    let host = std::env::var("APP_HOST")?;

    // Loading APP_PORT
    let port = std::env::var("APP_PORT")?;

    // Convert to u16
    let port: u16 = port.parse()?;

    Ok(format!("{host}:{port}"))
}

#[hooq]
fn main() -> Result<(), Box<dyn Error>> {
    let host_and_port = load_host_and_port()?;

    println!("Server is running on: {}", host_and_port);

    // snip

    Ok(())
}

#[test]
fn test_load_host_and_port() {
    unsafe {
        std::env::set_var("APP_HOST", "localhost");
        std::env::set_var("APP_PORT", "8080");
    }
    let host_and_port = load_host_and_port().unwrap();
    assert_eq!(host_and_port, "localhost:8080");
}

Simply adding #[hooq] magically produces a pseudo stack trace for errors:

$ APP_PORT=10 cargo run -q
[mdbook-source-code/index/src/main.rs:8:41] NotPresent
   8>    std::env::var("APP_HOST")?
    |
[mdbook-source-code/index/src/main.rs:21:45] NotPresent
  21>    load_host_and_port()?
    |
Error: NotPresent

Apparently the APP_HOST environment variable was missing. We can also see how the error propagated (line 8, then line 21, etc.).

What exactly did the hooq macro do? That will be explained later.


  1. The former try macro.

  2. “Couldn’t we just use unwrap at this scale?” I hate perceptive brats like you… Even so, not being able to use Result comfortably would be inconvenient, so the hooq macro remains meaningful.

Documentation Links

A collection of documentation links related to hooq.

Tutorial

This chapter introduces the basics of using hooq across three lessons plus a wrap‑up page.

  1. #[hooq] to capture error lines
  2. Customize hooks with #[hooq::method(...)]
  3. Create / use presets via flavors
  4. Summary

We assume the hooq crate has already been added.

Adding hooq

Add via cargo:

cargo add hooq

Or specify the latest version in Cargo.toml (see crates.io).

Some features exist, but commonly used ones are in the default feature set. See Features.

#[hooq] to Capture Error Lines

We continue with the example from Introduction to explain what the hooq macro does to source code and how to use it.

use std::error::Error;

use hooq::hooq;

#[hooq]
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
    // Loading APP_HOST
    let host = std::env::var("APP_HOST")?;

    // Loading APP_PORT
    let port = std::env::var("APP_PORT")?;

    // Convert to u16
    let port: u16 = port.parse()?;

    Ok(format!("{host}:{port}"))
}

#[hooq]
fn main() -> Result<(), Box<dyn Error>> {
    let host_and_port = load_host_and_port()?;

    println!("Server is running on: {}", host_and_port);

    // snip

    Ok(())
}

#[test]
fn test_load_host_and_port() {
    unsafe {
        std::env::set_var("APP_HOST", "localhost");
        std::env::set_var("APP_PORT", "8080");
    }
    let host_and_port = load_host_and_port().unwrap();
    assert_eq!(host_and_port, "localhost:8080");
}

Adding the #[hooq] attribute macro to a function inserts the following default hooq method inspect_err in front of each ?, and after the value of each return and tail expression (only when the function’s return type matches a hook target such as Result).

.inspect_err(|e| {
    let path = $path;
    let line = $line;
    let col = $col;
    let expr = ::hooq::summary!($source);

    ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})

Items like $path are hooq meta variables. See Meta Variables for full details; those used in the default method are:

MetaLiteral KindDescription
$pathstringRelative path from crate root to the file
$lineintegerLine where the hook is inserted
$colintegerColumn where the hook is inserted
$sourcetokens of target expressionThe original expression being hooked (for logging)

The function load_host_and_port expands roughly like this (formatting may differ). Using cargo expand will show similar output:

---
source: mdbook-source-code/index/tests/test.rs
expression: "format!(\"{stdout}\")"
---
#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::error::Error;
use hooq::hooq;
fn load_host_and_port() -> Result<String, Box<dyn Error>> {
    let host = std::env::var("APP_HOST")
        .inspect_err(|e| {
            let path = "mdbook-source-code/index/src/main.rs";
            let line = 8usize;
            let col = 41usize;
            let expr = "   8>    std::env::var(\"APP_HOST\")?\n    |";
            ::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
        })?;
    let port = std::env::var("APP_PORT")
        .inspect_err(|e| {
            let path = "mdbook-source-code/index/src/main.rs";
            let line = 11usize;
            let col = 41usize;
            let expr = "  11>    std::env::var(\"APP_PORT\")?\n    |";
            ::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
        })?;
    let port: u16 = port
        .parse()
        .inspect_err(|e| {
            let path = "mdbook-source-code/index/src/main.rs";
            let line = 14usize;
            let col = 33usize;
            let expr = "  14>    port.parse()?\n    |";
            ::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
        })?;
    Ok(format!("{0}:{1}", host, port))
}
fn main() -> Result<(), Box<dyn Error>> {
    let host_and_port = load_host_and_port()
        .inspect_err(|e| {
            let path = "mdbook-source-code/index/src/main.rs";
            let line = 21usize;
            let col = 45usize;
            let expr = "  21>    load_host_and_port()?\n    |";
            ::std::eprintln!("[{0}:{1}:{2}] {3:?}\n{4}\n", path, line, col, e, expr);
        })?;
    {
        ::std::io::_print(format_args!("Server is running on: {0}\n", host_and_port));
    };
    Ok(())
}

Warning: line!() macro is discouraged here.

You might think: “Why not just use line!()? Adding a $line meta variable increases cognitive load.”

This is intentional: line!() does not point to the line of the hooked expression; it points to the attribute line (#[hooq] or #[hooq::method(...)]).

We want the exact line of the ? or other target, hence $line exists.

Thanks to this expansion, running the program as shown in Introduction yields output identifying the precise lines where errors occurred:

$ APP_PORT=10 cargo run -q
[mdbook-source-code/index/src/main.rs:8:41] NotPresent
   8>    std::env::var("APP_HOST")?
    |
[mdbook-source-code/index/src/main.rs:21:45] NotPresent
  21>    load_host_and_port()?
    |
Error: NotPresent

The default method is helpful, but you will likely want to customize it. We cover customization in the next lesson.

Customize Hooks with #[hooq::method(...)]

You can configure which method is hooked.

We will show this using an example program that prints the name field from the project’s Cargo.toml. We add the toml crate first:

cargo add toml

Basic routine to parse a provided path as TOML:

use hooq::hooq;

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    // snip

    Ok(())
}

Instead of applying hooq as‑is, we will customize the hook. We also want success logging via inspect.

Use #[hooq::method(...)] to specify methods to insert. The meta variable .$so_far represents the chain built so far, letting us extend it with additional methods.

use hooq::hooq;

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let _cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    // snip

    Ok(())
}

Failure still logs as before:

[mdbook-source-code/tutorial-1/src/main.rs:10:81] Os { code: 2, kind: NotFound, message: "No such file or directory" }
  10>    std::fs::read_to_string(path)?
    |
Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

Success now logs too:

Success: `std :: fs :: read_to_string(path) ?` @ Line 10: Col: 81
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 10: Col: 83

Next we extract the package.name field with toml::Value::get. Because it returns an Option, we convert to Result using ok_or_else.

use hooq::hooq;

fn display_name(val: &toml::Value) -> Result<(), String> {
    let name = val
        .get("package")
        .ok_or_else(|| format!("get package [Line: {}]", line!()))?
        .get("name")
        .ok_or_else(|| format!("get name [Line: {}]", line!()))?
        .as_str()
        .ok_or_else(|| format!("as_str [Line: {}]", line!()))?;

    println!("name: {name}");

    Ok(())
}

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name(&cargo_toml)?;

    Ok(())
}

As you may guess: .ok_or_else(...) is boilerplate. hooq can reduce it:

use hooq::hooq;

#[hooq]
#[hooq::method(.ok_or_else(|| {
    format!("{} [Line: {}, {}]",
        stringify!($source),
        $line,
        $nth
    )
}))]
fn display_name(val: &toml::Value) -> Result<(), String> {
    let name = val.get("package")?.get("name")?.as_str()?;

    println!("name: {name}");

    Ok(())
}

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name(&cargo_toml)?;

    Ok(())
}

We also added $nth (alias of $count) to show which numbered ? caused failure. Running with an error:

Success: `std :: fs :: read_to_string(path) ?` @ Line 26: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 26: Col: 82
[mdbook-source-code/tutorial-3/src/main.rs:28:30] "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"
  28>    display_name(&cargo_toml)?
    |
Error: "val.get(\"package\") ? .get(\"name\") ? [Line: 12, 2nd ?]"

The TOML had package but not name; the second ? failed as indicated.

Skipping Hooks with #[hooq::skip_all]

Suppose we add a validation: “4 is ominous (like 4041), so reject names containing 4.”

use hooq::hooq;

#[hooq]
#[hooq::method(.ok_or_else(|| {
    format!("{} [Line: {}, {}]",
        stringify!($source),
        $line,
        $nth
    )
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
    let name = val.get("package")?.get("name")?.as_str()?;

    if name.contains("4") {
        return Err(format!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}

hooq also tries to hook return Err(...); but Result lacks ok_or_else, causing a compile error2:

error[E0599]: no method named `ok_or_else` found for enum `Result<T, E>` in the current scope
  --> mdbook-source-code/tutorial-4-compile-error/src/main.rs:15:9
   |
15 |         return Err(format!(
   |         ^^^^^^
   |
help: there is a method `or_else` with a similar name
   |
15 -         return Err(format!(
15 +         or_else Err(format!(
   |

For more information about this error, try `rustc --explain E0599`.
error: could not compile `tutorial-4-compile-error` (bin "tutorial-4-compile-error") due to 1 previous error

We do not want a hook here. Add #[hooq::skip_all] to skip inside that scope:

use hooq::hooq;

#[hooq]
#[hooq::method(.ok_or_else(|| {
    format!("{} [Line: {}, {}]",
        stringify!($source),
        $line,
        $nth
    )
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
    let name = val.get("package")?.get("name")?.as_str()?;

    #[hooq::skip_all]
    if name.contains("4") {
        return Err(format!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}
Complete program
use hooq::hooq;

#[hooq]
#[hooq::method(.ok_or_else(|| {
    format!("{} [Line: {}, {}]",
        stringify!($source),
        $line,
        $nth
    )
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
    let name = val.get("package")?.get("name")?.as_str()?;

    #[hooq::skip_all]
    if name.contains("4") {
        return Err(format!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq]
#[hooq::method(.$so_far.inspect(|_| {
    println!("Success: `{}` @ Line {}: Col: {}", stringify!($source), $line, $col);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}

Compilation succeeds and names containing 4 produce an error:

Success: `std :: fs :: read_to_string(path) ?` @ Line 33: Col: 80
Success: `toml :: from_str(& std :: fs :: read_to_string(path) ?) ?` @ Line 33: Col: 82
[mdbook-source-code/tutorial-4/src/main.rs:35:39] "name `tutorial-4` contains '4'. Guido Mista disallow this."
  35>    display_name_by_mista(&cargo_toml)?
    |
Error: "name `tutorial-4` contains '4'. Guido Mista disallow this."

Besides #[hooq::skip_all], multiple attributes exist to adjust behavior mid‑function (including attaching #[hooq::method(...)] to specific expressions). See Attributes.

Custom methods everywhere may get tedious. The next lesson introduces flavor presets.


  1. Generally, the number 4 is considered an unlucky number, but for server-side engineers, 5 may be even more unlucky.

  2. I apologies for the broken compilation error messages, I’ll fix them someday.

Create / Use Presets via Flavors

At the end of the previous lesson we saw ok_or_else missing on Result when trying to hook uniformly. Can we hook something available on both Result and Option? The Context::with_context method from anyhow fits perfectly: on Option it converts None into an anyhow::Result::Err.

use anyhow::{Context, Result};
use hooq::hooq;

#[hooq]
#[hooq::method(.with_context(|| {
    let path = $path;
    let line = $line;
    let col = $col;
    let expr = ::hooq::summary!($source);

    format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
    let name = val.get("package")?.get("name")?.as_str()?;

    if name.contains("4") {
        return Err(anyhow::anyhow!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq]
#[hooq::method(.with_context(|| {
    let path = $path;
    let line = $line;
    let col = $col;
    let expr = ::hooq::summary!($source);

    format!("[{path}:{line}:{col}]\n{expr}")
}))]
fn main() -> Result<()> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}

Run to produce an error: the extra .with_context(...) calls accumulate, giving a trace.

Error: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:41:39]
  41>    display_name_by_mista(&cargo_toml)?
    |

Caused by:
    0: [mdbook-source-code/tutorial-4-with-anyhow/src/main.rs:17:9]
         17>    return Err(anyhow::anyhow!(
         18|            "name `{name}` contains '4'. Guido Mista disallow this."
         19|        ))
           |
    1: name `tutorial-4-with-anyhow` contains '4'. Guido Mista disallow this.

Because this is a frequent pattern hooq provides a preset—the anyhow flavor. Presets are called “flavors” in hooq. There are also flavors for log, eyre, and tracing.

Change #[hooq] to #[hooq(anyhow)] to enable the flavor:

use anyhow::Result;
use hooq::hooq;

#[hooq(anyhow)]
fn display_name_by_mista(val: &toml::Value) -> Result<()> {
    let name = val.get("package")?.get("name")?.as_str()?;

    if name.contains("4") {
        return Err(anyhow::anyhow!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq(anyhow)]
fn main() -> Result<()> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}

Output matches the manual version.

See Flavors for all built‑in presets.

Define Custom Flavors

You can define flavors in a hooq.toml placed at the crate root (CARGO_MANIFEST_DIR). Example:

[my_flavor]
method = """.inspect_err(|_| {
    eprintln!("Error @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})
.inspect(|_| {
    println!("Success @ Line {}: Col: {}\n{}", $line, $col, ::hooq::summary!($source));
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Ok", "Err"]
result_types = ["Result"]
hook_in_macros = true

[my_flavor.ok_or_else]
method = """
.ok_or_else(|| {
    format!("[Line: {}, {}]\n{}",
        $line,
        $nth,
        ::hooq::summary!($source),
    )
})
.$so_far"""

Meaning of the keys:

KeyEffect
methodMethod to hook
hook_targetsWhich of ?, return, tail_expr to hook
tail_expr_identsIdents (e.g. Err) that force hooking on tail / return
result_typesReturn type idents (e.g. Result) whose tail/return values are considered for hooks

my_flavor.ok_or_else is a sub‑flavor, inheriting settings and overriding a subset.

For all available fields and precedence rules see Attributes and Flavors.

Use the flavor in code:

use hooq::hooq;

#[hooq(my_flavor)]
fn display_name_by_mista(val: &toml::Value) -> Result<(), String> {
    // Method can be overridden by the one in flavor!
    #[hooq::method = my_flavor::ok_or_else]
    let name = val.get("package")?.get("name")?.as_str()?;

    if name.contains("4") {
        return Err(format!(
            "name `{name}` contains '4'. Guido Mista disallow this."
        ));
    }

    println!("Mista「name: {name}」");

    Ok(())
}

#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let path = std::env::args().nth(1).unwrap_or("Cargo.toml".to_string());

    let cargo_toml: toml::Value = toml::from_str(&std::fs::read_to_string(path)?)?;

    display_name_by_mista(&cargo_toml)?;

    Ok(())
}

#[hooq::method = flavor_name] performs partial application of a flavor’s settings. Other fields also support this via #[hooq::field = flavor_name]. See Attributes (partial application).

Subtle note: $so_far represents the chain after initial flavor application; you cannot reference $so_far inside the base flavor method itself. Thus #[hooq(my_flavor::ok_or_else)] cannot include .$so_far, while #[hooq::method = my_flavor::ok_or_else] can.

Summary

Across three lessons we introduced core hooq usage.

There are further mechanisms and details not covered in the tutorial; consult the individual reference pages for deeper exploration.

  • Reference: Detailed explanations of each feature.
  • Recipes & Ideas: Usage examples of built‑in flavors, exploratory scenarios, and hidden tricks.

Comparison With Other Approaches

Applying #[hooq] (or #[hooq(anyhow)]) to every function yields a pseudo stack trace for errors. Here is a comparison with other strategies for obtaining stack‑like traces:

Backtracetracinghooq
Learning cost / flexibility⚠️⚠️🌈
Ease of type definitions⚠️
Macro‑less🌈
Information volume control⚠️🌈
Platform support⚠️🌈

Legend:

  • 🌈: Excellent
  • ✅: Good
  • ⚠️: So‑so
  • ❌: Poor

Explanation:

  • Learning cost / flexibility
    • ⚠️ Backtrace requires setting RUST_LIB_BACKTRACE=1 and relies on OS thread info; extra system knowledge helps.
    • ⚠️ tracing is powerful but may be overkill if you only want a stack trace analogue.
    • 🌈 hooq requires only adding an attribute on functions.
  • Ease of type definitions
    • ⚠️ With thiserror + Backtrace, you must pre‑plan fields; retrofitting is harder as error types multiply.
    • tracing imposes no such constraints.
    • hooq cooperates with arbitrary error crates.
  • Macro‑less
    • 🌈 Backtrace needs no macros.
    • tracing generally needs #[tracing::instrument] for effortless spans.
    • hooq is an attribute macro by design.
  • Information volume control
    • ⚠️ Raw Backtrace output is often too verbose (and weak for async); pairing with color-eyre helps.
    • tracing gives structured spans; precise line of each ? still requires manual logging.
    • 🌈 hooq pinpoints only annotated functions and the exact ? / return / tail expression locations.
      • Conditional usage via #[cfg_attr(..., hooq(...))] enables feature/test scoped tracing.
      • 💡 Combine with tracing to augment granularity—see tracing flavor.
  • Platform support
    • ⚠️ Backtrace has platform caveats (see official docs ).
    • ✅ Standard logging with tracing is widely portable.
    • 🌈 hooq merely inserts methods; no platform primitives required.
      • 💡 #[hooq::method(.unwrap()!)] can alias ? to .unwrap() behind a feature.

Reference

This section covers details not addressed in the tutorial.

Contents

PageSummary
AttributesExplains #[hooq(...)] and inert attributes like #[hooq::attr(...)]. Mainly controls macro behavior.
MethodHow hooked methods are specified and inserted/replaced.
Meta VariablesVariables available inside hooked methods for logging/debugging.
FlavorsPreset configurations that bundle common settings; both built‑in and user‑defined.
FeaturesCargo features provided by the hooq crate.

Attributes

The behavior of the hooq macro is controlled by the root meta on #[hooq(...)] and by inert attributes inserted afterwards (#[hooq::attribute(...)]). This page explains configurable items and defaults.

Quick Reference

NameKindDescription
flavorroot metaApply settings from a named flavor.
trait_useroot metaInsert use XXX as _; before the item for the given trait path(s).
methodinertConfigure the method to insert/replace.
skip_all / skipinertSkip hooking the annotated expression; skip_all also skips inside it.
hook_targetsinertEnable hooking on ?, return, and/or tail_expr (default: all).
tail_expr_identsinertIdents to hook when appearing as tail/return values (default: Err).
ignore_tail_expr_identsinertIdents to avoid hooking even if otherwise eligible (default: Ok).
result_typesinertFunction return type idents considered hook targets for return/tail (default: Result).
hook_in_macrosinertWhether to hook inside macro invocations (default: true).
bindinginertDefine custom meta variable bindings (expression or literal).
  • Root meta: attributes written inside #[hooq(...)].
  • Inert attributes: #[hooq::attribute(...)] placed in various positions after the macro.

Example with all attributes:

use hooq::hooq;

mod sub {
    pub trait Trait {}
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(flavor = "hook", trait_use(sub::Trait))] // Attribute macro root.
#[hooq::method(.inspect_err(|_| { let _ = "error!"; }))] // All following attributes are inert.
#[hooq::hook_targets("?", "return", "tail_expr")]
#[hooq::tail_expr_idents("Err")]
#[hooq::ignore_tail_expr_idents("Ok")]
#[hooq::result_types("Result")]
#[hooq::hook_in_macros(true)]
#[hooq::binding(xxx = "xxx_value")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::skip_all]
    if failable(false)? {
        failable(())?;
    }

    #[hooq::skip]
    if failable(false)? {
        // Next line is not skipped.
        failable(())?;
    }

    #[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
    failable(())?;

    Ok(())
}

Expansion:

use hooq::hooq;
mod sub {
    pub trait Trait {}
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
#[allow(unused)]
use sub::Trait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "error!";
        })?;
    if failable(false)? {
        failable(())?;
    }
    if failable(false)? {
        failable(())
            .inspect_err(|_| {
                let _ = "error!";
            })?;
    }
    failable(())
        .inspect_err(|_| {
            let _ = "xxx_value";
        })?;
    Ok(())
}

Root Meta

Two fields are available inside #[hooq(...)]:

NameSyntax
flavor#[hooq(FLAVOR_NAME)] or #[hooq(flavor = "FLAVOR_NAME")]
trait_use#[hooq(trait_use(PATH, ...))] or #[hooq(trait_uses(PATH, ...))]

flavor

Apply base settings from a flavor:

use hooq::hooq;

#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    func()?;

    Ok(())
}

#[hooq(my_flavor::sub_flavor)]
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(flavor = "my_flavor.sub_flavor")]
fn func() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

hooq.toml example:

[my_flavor]
method = """.inspect(|_| {
    let _ = $tag;
})"""
bindings = { tag = "\"my_flavor\"" }

[my_flavor.sub_flavor]
bindings = { tag = "\"my_flavor.sub_flavor\"" }
tail_expr_idents = ["Ok"]

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    func()
        .inspect(|_| {
            let _ = "my_flavor";
        })?;
    Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
        .inspect(|_| {
            let _ = "my_flavor.sub_flavor";
        })
}
fn func() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect(|_| {
            let _ = "my_flavor.sub_flavor";
        })?;
    Ok(())
        .inspect(|_| {
            let _ = "my_flavor.sub_flavor";
        })
}

Sub‑flavors inherit and override parent flavor settings. Use dotted or path‑like names (e.g. base.sub or base::sub), and they can nest.

Further info, especially built‑ins like anyhow, writing hooq.toml, etc.: see Flavors.

trait_use

Insert #[allow(unused)] use PATH as _; above the item:

use hooq::hooq;

mod sub {
    pub trait Inserted {
        fn inserted(self) -> Self;
    }

    impl<T, E> Inserted for Result<T, E> {
        fn inserted(self) -> Self {
            self
        }
    }
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(trait_use(sub::Inserted))]
#[hooq::method(.inserted())]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion excerpt:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod sub {
    pub trait Inserted {
        fn inserted(self) -> Self;
    }
    impl<T, E> Inserted for Result<T, E> {
        fn inserted(self) -> Self {
            self
        }
    }
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
#[allow(unused)]
use sub::Inserted as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(()).inserted()?;
    Ok(())
}

Useful when the hooked method requires a trait (e.g. anyhow::Context for .with_context(...)). You can also configure trait_uses in hooq.toml so a flavor brings the necessary imports.

method

NameSyntax
method#[hooq::method(...)]

Set the method via inert attribute. Works both immediately under #[hooq] and inside the function.

use hooq::hooq;

#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = "specified @ root"; }))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::method(.inspect_err(|_| { let _ = "specified @ inner"; }))]
    failable(())?;

    Ok(())
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "specified @ root";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "specified @ inner";
        })?;
    Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

Two modes exist:

  • Insertion mode: starts with .; inserts between the expression and ? (or at the end for return/tail).
  • Replacement mode: otherwise; replace the target using an expression. Use $expr to access the original (already recursively hooked) expression.

Meta variables like $line, $source, $fn_name are available. Default flavor inserts:

.inspect_err(|e| {
    let path = $path;
    let line = $line;
    let col = $col;
    let expr = ::hooq::summary!($source);

    ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
})

See Method and Meta Variables.

Skipping Hooks

NameSyntax
skip_all#[hooq::skip_all]
skip#[hooq::skip]

skip_all

Prevent hooks inside the annotated expression:

use hooq::hooq;

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::skip_all]
    let f = || -> Option<()> {
        optional(())?; // If the hook is applied, an compile error occurs.

        Some(())
    };

    let _ = failable(f())?;

    Ok(())
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

fn optional<T>(val: T) -> Option<T> {
    Some(val)
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(()).inspect_err(|_| {})?;
    let f = || -> Option<()> {
        optional(())?;
        Some(())
    };
    let _ = failable(f()).inspect_err(|_| {})?;
    Ok(())
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn optional<T>(val: T) -> Option<T> {
    Some(val)
}

skip

Skip only within the parent scope (children can still be hooked). Helpful when nested tail expressions would produce noisy logs.

use hooq::hooq;

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> Result<(), String> {
    match failable(failable(()))? {
        Ok(()) => Ok(()),
        Err(s) => Err(s),
    }
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func2() -> Result<(), String> {
    #[hooq::skip]
    match failable(failable(()))? {
        Ok(()) => Ok(()),
        Err(s) => Err(s),
    } // Not hooked here.
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

fn main() {
    let _ = func1();
    let _ = func2();
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn func1() -> Result<(), String> {
    match failable(failable(())).inspect_err(|_| {})? {
        Ok(()) => Ok(()),
        Err(s) => Err(s).inspect_err(|_| {}),
    }
        .inspect_err(|_| {})
}
fn func2() -> Result<(), String> {
    match failable(failable(()))? {
        Ok(()) => Ok(()),
        Err(s) => Err(s).inspect_err(|_| {}),
    }
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() {
    let _ = func1();
    let _ = func2();
}

Control Hook Targets

Inert attributes to tweak whether/how hooks apply.

NameSyntaxValues
hook_targets#[hooq::hook_targets(...)]Any of "?", "return", "tail_expr".
tail_expr_idents#[hooq::tail_expr_idents(...)]Idents like Err.
ignore_tail_expr_idents#[hooq::ignore_tail_expr_idents(...)]Idents like Ok.
result_types#[hooq::result_types(...)]Return type idents like Result.
hook_in_macros#[hooq::hook_in_macros(...)]true or false.

Priority rules:

  • If skip_all is present, do not hook (for skip, skip only in the same scope excluding children).
  • If target is inside a macro call (like println!(...) ) and hook_in_macros is false, do not hook.
  • For ?: hook if included in hook_targets.
  • For return: hook if included in hook_targets, and either ident is in tail_expr_idents or return type ident is in result_types and ident is not in ignore_tail_expr_idents.
  • For tail: similar rules as return.

hook_targets

You can specify whether to hook each of the following three types: the ? operator (Question Operator), return, and tail expressions (tail_expr). By default, all three types are hooked.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("?")]
fn target_question() -> Result<(), String> {
    failable(())?;

    if failable(false)? {
        return Err("error".into());
    }

    if failable(true)? {
        Ok(())
    } else {
        Err("error".into())
    }
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("return")]
fn target_return() -> Result<(), String> {
    failable(())?;

    if failable(false)? {
        return Err("error".into());
    }

    if failable(true)? {
        Ok(())
    } else {
        Err("error".into())
    }
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::hook_targets("tail_expr")]
fn target_tail_expr() -> Result<(), String> {
    failable(())?;

    if failable(false)? {
        return Err("error".into());
    }

    if failable(true)? {
        Ok(())
    } else {
        Err("error".into())
    }
}

fn main() {
    let _ = target_question();
    let _ = target_return();
    let _ = target_tail_expr();
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn target_question() -> Result<(), String> {
    failable(()).inspect_err(|_| {})?;
    if failable(false).inspect_err(|_| {})? {
        return Err("error".into());
    }
    if failable(true).inspect_err(|_| {})? { Ok(()) } else { Err("error".into()) }
}
fn target_return() -> Result<(), String> {
    failable(())?;
    if failable(false)? {
        return Err("error".into()).inspect_err(|_| {});
    }
    if failable(true)? { Ok(()) } else { Err("error".into()) }
}
fn target_tail_expr() -> Result<(), String> {
    failable(())?;
    if failable(false)? {
        return Err("error".into());
    }
    if failable(true)? { Ok(()) } else { Err("error".into()).inspect_err(|_| {}) }
        .inspect_err(|_| {})
}
fn main() {
    let _ = target_question();
    let _ = target_return();
    let _ = target_tail_expr();
}

tail_expr_idents

Idents to hook regardless of result_types. Default: Err.

Identifiers must be single idents (e.g., Zzz), not paths like xxx::yyy::Zzz.

Matching uses the last segment of a path (xxx::yyy::Zzz matches by Zzz).

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), String> {
    let _: Result<(), String> = {
        let _: Result<(), String> = {
            let res = "error".to_string();
            Err(res)
        };

        failable(())
    };

    #[hooq::tail_expr_idents("Err", "failable")]
    let _: Result<(), String> = {
        let _: Result<(), String> = {
            let res = "error".to_string();
            Err(res)
        };

        failable(()) // This will be hooked because of tail_expr_idents.
    };

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), String> {
    let _: Result<(), String> = {
        let _: Result<(), String> = {
            let res = "error".to_string();
            Err(res).inspect_err(|_| {})
        };
        failable(())
    };
    let _: Result<(), String> = {
        let _: Result<(), String> = {
            let res = "error".to_string();
            Err(res).inspect_err(|_| {})
        };
        failable(()).inspect_err(|_| {})
    };
    Ok(())
}

ignore_tail_expr_idents

Avoid hooking these idents even when otherwise eligible. Default: Ok.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() {
    let f = || -> Result<(), String> { failable(()) };

    #[hooq::ignore_tail_expr_idents("failable")]
    let g = || -> Result<(), String> { failable(()) };

    #[hooq::tail_expr_idents("!failable")]
    let h = || -> Result<(), String> { failable(()) };

    f().unwrap();
    g().unwrap();
    h().unwrap();
}

As shown above, you can also achieve the same behavior without ignore_tail_expr_idents by prefixing an ident with ! in tail_expr_idents (e.g., !Ok).

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() {
    let f = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
    let g = || -> Result<(), String> { failable(()) };
    let h = || -> Result<(), String> { failable(()) };
    f().unwrap();
    g().unwrap();
    h().unwrap();
}

If the same ident appears in both lists, it will be hooked due to simple mechanics; prefer using !Ident via tail_expr_idents instead.

result_types

Function return type idents considered for return/tail hooks. Default: Result.

As with tail_expr_idents, identifiers must be single idents (e.g., Zzz), not full paths like xxx::yyy::Zzz.

Matching uses the final segment of a path (xxx::yyy::Zzz matches by Zzz).

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

type MyResult = Result<(), String>;

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn func1() -> MyResult {
    let _ = || -> Result<(), String> { failable(()) };

    failable(())
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("MyResult")]
fn func2() -> MyResult {
    // No longer hooked.
    let _ = || -> Result<(), String> { failable(()) };

    failable(())
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
#[hooq::result_types("Result", "MyResult")]
fn func3() -> MyResult {
    let _ = || -> Result<(), String> { failable(()) };

    let _ = || {
        // Not hooked because return type of the closure is unknown.
        failable(())
    };

    failable(())
}

fn main() {
    let _ = func1();
    let _ = func2();
    let _ = func3();
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
type MyResult = Result<(), String>;
fn func1() -> MyResult {
    let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
    failable(())
}
fn func2() -> MyResult {
    let _ = || -> Result<(), String> { failable(()) };
    failable(()).inspect_err(|_| {})
}
fn func3() -> MyResult {
    let _ = || -> Result<(), String> { failable(()).inspect_err(|_| {}) };
    let _ = || { failable(()) };
    failable(()).inspect_err(|_| {})
}
fn main() {
    let _ = func1();
    let _ = func2();
    let _ = func3();
}

hook_in_macros

Control hooking inside function‑like macros.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("{}", failable("hello")?);

    #[hooq::hook_in_macros(false)]
    println!("{}", failable("world")?);

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    {
        ::std::io::_print(format_args!("{0}\n", failable("hello").inspect_err(|_| {})?));
    };
    {
        ::std::io::_print(format_args!("{0}\n", failable("world")?));
    };
    Ok(())
}

The arguments to function-like macros do not always follow standard Rust syntax, and hooking them incurs a small parsing cost. This inert attribute allows you to disable hooking inside macros. If you don’t need to hook inside macros, setting this to false may slightly reduce compile times.

binding

Define user meta variables (bindings). Several forms:

SyntaxNote
#[hooq::binding(xxx = ...)]
#[hooq::var(xxx = ...)]
#[hooq::xxx = ...]xxx must not conflict with other inert attribute names.
use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| { let _ = $xxx; }))]
#[hooq::xxx = 10]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::binding(xxx = "in block")]
    {
        failable(())?;

        #[hooq::var(xxx = 42)]
        failable(())?;

        failable(())?;
    }

    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = 10;
        })?;
    {
        failable(())
            .inspect_err(|_| {
                let _ = "in block";
            })?;
        failable(())
            .inspect_err(|_| {
                let _ = 42;
            })?;
        failable(())
            .inspect_err(|_| {
                let _ = "in block";
            })?;
    }
    failable(())
        .inspect_err(|_| {
            let _ = 10;
        })?;
    Ok(())
}

See also Meta Variables — Bindings.

Partial Application via Flavor

You can partially apply flavor settings via inert attributes:

  • #[hooq::attribute = flavor_name] for a single field.
  • #[hooq::flavor = flavor_name] to override all inert‑attribute fields.
  • #[hooq::bindings = flavor_name] to override existing user bindings.
use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

type MyResult = Result<(), String>;

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _x = $xxx;
    let _y = $yyy;
}))]
#[hooq::xxx = "from root"]
#[hooq::yyy = "from root"]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    // Not hooked.
    let _ = || -> MyResult { failable(()) };

    #[hooq::method = "my_flavor"]
    // Method will be changed.
    failable(())?;

    #[hooq::result_types = "my_flavor"]
    // Hooked now.
    let _ = || -> MyResult { failable(()) };

    #[hooq::bindings = "my_flavor"]
    // Bindings will be changed.
    failable(())?;

    #[hooq::flavor = "my_flavor"]
    // All will be changed.
    failable(())?;

    Ok(())
}

hooq.toml:

[my_flavor]
method = """.inspect_err(|_| {
    let _ = "from my_flavor";
    let _x = $xxx;
    let _y = $yyy;
})"""
result_types = ["Result", "MyResult"]
bindings = { xxx = "\"xxx from my_flavor\"", yyy = "\"yyy from my_flavor\"" }

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
type MyResult = Result<(), String>;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _x = "from root";
            let _y = "from root";
        })?;
    let _ = || -> MyResult { failable(()) };
    failable(())
        .inspect_err(|_| {
            let _ = "from my_flavor";
            let _x = "from root";
            let _y = "from root";
        })?;
    let _ = || -> MyResult {
        failable(())
            .inspect_err(|_| {
                let _x = "from root";
                let _y = "from root";
            })
    };
    failable(())
        .inspect_err(|_| {
            let _x = "xxx from my_flavor";
            let _y = "yyy from my_flavor";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "from my_flavor";
            let _x = "xxx from my_flavor";
            let _y = "yyy from my_flavor";
        })?;
    Ok(())
}

Method

Specify hooked methods via #[hooq::method(...)] or by flavors. The ... has two modes:

  • If it starts with ., it is in insertion mode, inserting the method between the expression and ? (or at the end for return/tail).
  • Otherwise it is in replacement mode, replacing the expression. Use $expr to reference the original expression (already recursively hooked where needed).

Meta variables (see Meta Variables) can be used in these expressions.

Insertion Mode

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::method(.inspect_err(|_| {
        let _ = "before chainned";
    }).$so_far)]
    failable(())?;

    #[hooq::method(.$so_far.inspect_err(|_| {
        let _ = "after chainned";
    }))]
    failable(())?;

    Ok(())
}

You can chain with .$so_far to prepend/append relative to the existing chain.

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "inserted mode";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "before chainned";
        })
        .inspect_err(|_| {
            let _ = "inserted mode";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "inserted mode";
        })
        .inspect_err(|_| {
            let _ = "after chainned";
        })?;
    Ok(())
}

Replacement Mode

If it does not start with . (dot), it is treated as a replacement function, replacing the original expression with the configured expression. The expression being replaced can be accessed using the $expr meta variable. (While it’s possible to write without using $expr, it would likely not be useful.)

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
    E: std::fmt::Debug,
{
    if let Err(e) = &r {
        println!("Error occurred: {:?}", e);
    }

    r
}

#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
    E: std::fmt::Debug,
{
    if let Err(e) = &r {
        {
            ::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
        };
    }
    r
}
fn main() -> Result<(), String> {
    wrapper(failable(()))?;
    Ok(())
}

Use $expr (not $source) in replacement mode. $expr holds the expression after inner hooks have been applied, while $source is for logging/display of the original tokens.

Removing ? via Trailing !

When hooking the ? operator, appending ! to the method consumes the trailing ?.

Requires the consume-question feature.

cargo add hooq --features consume-question

Example aliasing ? to .unwrap():

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, &'static str> {
    Ok(val)
}

#[hooq]
#[hooq::hook_targets("?")]
#[hooq::method(.unwrap()!)]
fn main() {
    failable(())?;
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, &'static str> {
    Ok(val)
}
fn main() {
    failable(()).unwrap();
}

Meta Variables

Inside #[hooq::method(...)], you can use special meta variables (prefixed with $) that are replaced with values.

Quick Reference

NameLiteral KindDescription
$lineusizeLine number of the target.
$column or $colusizeColumn number of the target.
$pathstringRelative path to the file of the target.
$filestringFile name of the target.
$sourceexpressionOriginal tokens of the target (for logging; differs from $expr).
$count or $nthstringDisplay which numbered target within the function.
$fn_name or $fnnamestringName of the function containing the target.
$fn_sig or $fnsigstringSignature of the function/closure containing the target.
$xxx(any)User‑defined meta variable via inert attribute.
$bindings or $varsHashMapAll user bindings.
$hooq_meta or $hooqmetahooq::HooqMetaStruct bundling key meta information.
$exprexpressionOriginal expression for replacement mode (after inner hooks).
$so_far or $sofarexpressionCurrent insertion chain, used to chain further.

Example using most variables:

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::xxx = "user defined binding."]
#[hooq::method(.inspect_err(|_| {
    // Fundamental information provided by hooq.
    let _line = $line;
    let _column = $column;
    let _path = $path;
    let _file = $file;
    let _source = stringify!($source);
    let _count = $count;
    let _fn_name = $fn_name;
    let _fn_sig = $fn_sig;

    // Meta vars defined by user.
    let _xxx = $xxx;
    let _bindings = $bindings;

    // All information summarized up to this point.
    let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _line = 28usize;
            let _column = 17usize;
            let _path = "mdbook-source-code/meta-vars-all/src/main.rs";
            let _file = "main.rs";
            let _source = "failable(()) ?";
            let _count = "1st ?";
            let _fn_name = "main";
            let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
            let _xxx = "user defined binding.";
            let _bindings = ::std::collections::HashMap::from([
                (
                    ::std::string::ToString::to_string("xxx"),
                    {
                        let expr = ::std::string::ToString::to_string(
                            "\"user defined binding.\"",
                        );
                        let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                            "user defined binding.",
                        );
                        ::hooq::BindingPayload {
                            expr,
                            value,
                        }
                    },
                ),
            ]);
            let _hooq_meta = ::hooq::HooqMeta {
                line: 28usize,
                column: 17usize,
                path: "mdbook-source-code/meta-vars-all/src/main.rs",
                file: "main.rs",
                source_str: "failable(()) ?",
                count: "1st ?",
                bindings: ::std::collections::HashMap::from([
                    (
                        ::std::string::ToString::to_string("xxx"),
                        {
                            let expr = ::std::string::ToString::to_string(
                                "\"user defined binding.\"",
                            );
                            let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                                "user defined binding.",
                            );
                            ::hooq::BindingPayload {
                                expr,
                                value,
                            }
                        },
                    ),
                ]),
            };
        })?;
    Ok(())
}

Target Information

line

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _line = $line;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _line = 12usize;
        })?;
    Ok(())
}

Prefer $line over line!(); the latter points to the attribute location, not the actual target.

column

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _column = $column;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _column = 17usize;
        })?;
    Ok(())
}

path

Replaced with the relative path from the crate root (CARGO_MANIFEST_DIR) to the file containing the hook target.

However, the relative path’s starting point may not be the crate root in cases such as when using workspaces. Since procedural macros are designed not to obtain accurate absolute file paths, hooq does not provide meta variables for retrieving absolute paths.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _path = $path;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _path = "mdbook-source-code/meta-vars-path/src/main.rs";
        })?;
    Ok(())
}

file

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _file = $file;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _file = "main.rs";
        })?;
    Ok(())
}

source

This meta variable provides the token stream (expression) of the hook target before any hooks are applied by the hooq macro. It is intended for debugging purposes.

In contrast, $expr represents the expression after hooks have already been applied internally. $expr is used in replacement mode to determine where to embed the original expression.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _source = stringify!($source);
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _source = "failable(()) ?";
        })?;
    Ok(())
}

Stringification helpers commonly used with $source:

Example with summary!:

use hooq::hooq;

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let source = ::hooq::summary!($source);

    eprintln!("{source}");
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Err((
        "aaaaaaaaaaaaaaaaaaaa",
        "bbbbbbbbbbbbbbbbbbbb",
        "cccccccccccccccccccc",
        "dddddddddddddddddddd",
        "errorerrorerrorerrorerror",
    )
        .4
        .into())
}

Result excerpt:

  10>    Err((
...
  15|        "erro..rror",
  16|    )
  17|        .4
  18|        .into())
    |

count

Indicates which numbered target this is within the function for each target type (?, return, or tail expression).

This meta variable is a remnant from when $line could only be obtained on nightly. In most cases, $line is more straightforward.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _count = $count;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _count = "1st ?";
        })?;
    Ok(())
}

fn_name

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _fn_name = $fn_name;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    (|| -> Result<(), String> {
        failable(())?;

        Ok(())
    })()?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _fn_name = "main";
        })?;
    (|| -> Result<(), String> {
        failable(())
            .inspect_err(|_| {
                let _fn_name = "__closure_in_main__";
            })?;
        Ok(())
    })()
        .inspect_err(|_| {
            let _fn_name = "main";
        })?;
    Ok(())
}

fn_sig

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _fn_sig = $fn_sig;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    (|| -> Result<(), String> {
        failable(())?;

        Ok(())
    })()?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
        })?;
    (|| -> Result<(), String> {
        failable(())
            .inspect_err(|_| {
                let _fn_sig = "| | -> Result < (), String > {}";
            })?;
        Ok(())
    })()
        .inspect_err(|_| {
            let _fn_sig = "fn main() -> Result < (), Box < dyn std :: error :: Error > >";
        })?;
    Ok(())
}

User‑Defined Meta Variables (Bindings)

Define via inert attributes #[hooq::xxx = ...] or in hooq.toml under bindings.

For details on how to define them, see the respective pages:

Example (inert attribute):

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

enum CauseKind {
    DataBase,
    Server,
}

#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
    let _string = $string;
    let _integer = $integer;
    let _cause_kind = $cause_kind;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    // Overriding meta variables.
    #[hooq::cause_kind = CauseKind::DataBase]
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
enum CauseKind {
    DataBase,
    Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _string = "hello!";
            let _integer = 10;
            let _cause_kind = CauseKind::Server;
        })?;
    failable(())
        .inspect_err(|_| {
            let _string = "hello!";
            let _integer = 10;
            let _cause_kind = CauseKind::DataBase;
        })?;
    Ok(())
}

bindings

Retrieve all user-defined meta variables (bindings) as a HashMap<String, BindingPayload>.

BindingPayload stores the stringified binding expression and a value using Rc<dyn Any>.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

enum CauseKind {
    #[allow(unused)]
    DataBase,
    Server,
}

#[hooq]
// Can be defined in the format #[hooq::xxx = value]
#[hooq::string = "hello!"] // string literal
#[hooq::integer = 10] // integer literal
#[hooq::cause_kind = CauseKind::Server] // some value
#[hooq::method(.inspect_err(|_| {
    let _bindings = $bindings;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
enum CauseKind {
    #[allow(unused)]
    DataBase,
    Server,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _bindings = ::std::collections::HashMap::from([
                (
                    ::std::string::ToString::to_string("cause_kind"),
                    {
                        let expr = ::std::string::ToString::to_string(
                            "CauseKind :: Server",
                        );
                        let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                            CauseKind::Server,
                        );
                        ::hooq::BindingPayload {
                            expr,
                            value,
                        }
                    },
                ),
                (
                    ::std::string::ToString::to_string("integer"),
                    {
                        let expr = ::std::string::ToString::to_string("10");
                        let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                            10,
                        );
                        ::hooq::BindingPayload {
                            expr,
                            value,
                        }
                    },
                ),
                (
                    ::std::string::ToString::to_string("string"),
                    {
                        let expr = ::std::string::ToString::to_string("\"hello!\"");
                        let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                            "hello!",
                        );
                        ::hooq::BindingPayload {
                            expr,
                            value,
                        }
                    },
                ),
            ]);
        })?;
    Ok(())
}

hooq_meta

Bundle meta info into hooq::HooqMeta.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::string = "hello!"]
#[hooq::method(.inspect_err(|_| {
    let _hooq_meta = $hooq_meta;
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _hooq_meta = ::hooq::HooqMeta {
                line: 13usize,
                column: 17usize,
                path: "mdbook-source-code/meta-vars-hooq_meta/src/main.rs",
                file: "main.rs",
                source_str: "failable(()) ?",
                count: "1st ?",
                bindings: ::std::collections::HashMap::from([
                    (
                        ::std::string::ToString::to_string("string"),
                        {
                            let expr = ::std::string::ToString::to_string("\"hello!\"");
                            let value: ::std::rc::Rc<dyn ::std::any::Any> = ::std::rc::Rc::new(
                                "hello!",
                            );
                            ::hooq::BindingPayload {
                                expr,
                                value,
                            }
                        },
                    ),
                ]),
            };
        })?;
    Ok(())
}

See also the hook flavor.

Advanced Meta Variables for Hook Construction

The meta variables introduced so far are primarily for obtaining meta information for logging and debugging.

The remaining two, $expr and $so_far, are meta variables that assist in making methods.

expr

Use in replacement mode to access the (already hooked internally) expression.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
    E: std::fmt::Debug,
{
    if let Err(e) = &r {
        println!("Error occurred: {:?}", e);
    }

    r
}

#[hooq]
#[hooq::method(wrapper($expr))]
fn main() -> Result<(), String> {
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn wrapper<T, E>(r: Result<T, E>) -> Result<T, E>
where
    E: std::fmt::Debug,
{
    if let Err(e) = &r {
        {
            ::std::io::_print(format_args!("Error occurred: {0:?}\n", e));
        };
    }
    r
}
fn main() -> Result<(), String> {
    wrapper(failable(()))?;
    Ok(())
}

so_far

Represents the chain of insertion‑mode hooks so far. Drop the leading dot when stored; write as .$so_far when inserting.

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq]
#[hooq::method(.inspect_err(|_| {
    let _ = "inserted mode";
}))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::method(.inspect_err(|_| {
        let _ = "before chainned";
    }).$so_far)]
    failable(())?;

    #[hooq::method(.$so_far.inspect_err(|_| {
        let _ = "after chainned";
    }))]
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "inserted mode";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "before chainned";
        })
        .inspect_err(|_| {
            let _ = "inserted mode";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "inserted mode";
        })
        .inspect_err(|_| {
            let _ = "after chainned";
        })?;
    Ok(())
}

Flavors

Flavors are presets that bundle hooq settings. Built‑in flavors:

NamefeatureContents
default-Default when nothing is specified; overridable via hooq.toml.
empty-Disables hooking; inert attributes still processed. Not overridable.
hook-Inserts a hook method taking hooq::HooqMeta; designed for user traits. Overridable.
anyhowanyhowInserts .with_context(...). Overridable.
eyreeyreInserts .wrap_err_with(...). Overridable.
loglogInserts inspect_err that calls ::log::error!. Overridable.
tracingtracingInserts inspect_err that calls ::tracing::error!. Overridable.

Flavor features are part of the default feature set, so you usually do not need to enable them explicitly.

Users can define flavors in a hooq.toml at crate root.

User‑Defined Flavors

hooq.toml uses table names as flavor names with fields:

FieldTypeDescription
trait_usesarray of stringsTrait paths to import.
methodstringMethod/expression to insert/replace.
hook_targetsarray of stringsAny of "?", "return", "tail_expr".
tail_expr_identsarray of stringsIdents like "Err".
ignore_tail_expr_identsarray of stringsIdents like "Ok".
result_typesarray of stringsReturn type idents like "Result".
hook_in_macrosbooltrue or false.
bindingsinline tableArbitrary bindings; note string literals must be quoted with \".

All built‑in (except empty) can be overridden by defining the same table name. Sub‑tables other than bindings are sub‑flavors and inherit from their parent.

See Attributes for how to apply flavors.

Example hooq.toml:

[my_flavor]
trait_uses = ["sub::MyTrait"]
method = """.inspect_err(|_| {
    let _ = $xxx;
})"""
hook_targets = ["?", "return", "tail_expr"]
tail_expr_idents = ["Err"]
ignore_tail_expr_idents = ["Ok"]
result_types = ["Result"]
hook_in_macros = true
bindings = { xxx = "\"my_flavor\"" }

[my_flavor.sub_flavor]
bindings = { xxx = "\"my_flavor_sub\"" }

Usage:

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

mod sub {
    pub trait MyTrait {}
}

#[hooq(my_flavor)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    #[hooq::flavor = my_flavor::sub_flavor]
    failable(())?;

    Ok(())
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
mod sub {
    pub trait MyTrait {}
}
#[allow(unused)]
use sub::MyTrait as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .inspect_err(|_| {
            let _ = "my_flavor";
        })?;
    failable(())
        .inspect_err(|_| {
            let _ = "my_flavor_sub";
        })?;
    Ok(())
}

default

Default configuration when using #[hooq].

It is configured as follows. (To keep the documentation consistent, this is excerpted directly from the source code; the same applies below.)

use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};

use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};

pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

mod flavor_path;
mod presets;
mod toml_load;

#[derive(Debug, Clone)]
pub struct Flavor {
    pub trait_uses: Vec<Path>,
    pub method: Method,
    pub hook_targets: HookTargetSwitch,
    pub tail_expr_idents: Vec<String>,
    pub ignore_tail_expr_idents: Vec<String>,
    pub result_types: Vec<String>,
    pub hook_in_macros: bool,
    pub bindings: HashMap<String, Rc<Expr>>,
    pub sub_flavors: HashMap<String, Flavor>,
}

impl Default for Flavor {
    fn default() -> Self {
        Self {
            trait_uses: Vec::new(),
            method: default_method(),
            hook_targets: HookTargetSwitch {
                question: true,
                return_: true,
                tail_expr: true,
            },
            tail_expr_idents: vec!["Err".to_string()],
            ignore_tail_expr_idents: vec!["Ok".to_string()],
            result_types: vec!["Result".to_string()],
            hook_in_macros: true,
            bindings: HashMap::new(),
            sub_flavors: HashMap::new(),
        }
    }
}

fn default_method() -> Method {
    // NOTE:
    // $path や $line は eprintln! に直接埋め込みたいところだが、
    // CI側のテストの関係でこのようになっている

    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    let res: TokenStream = parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
        })
    };

    Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}

#[derive(Debug)]
pub struct FlavorStore {
    flavors: HashMap<String, Flavor>,
}

// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
    pub inner: HooqToml,
}

pub struct TomlStore {
    inner: Mutex<HashMap<String, CheckedHooqToml>>,
}

pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
    inner: Mutex::new(HashMap::new()),
});

impl TomlStore {
    fn load() -> Result<Option<CheckedHooqToml>, String> {
        if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
            return Ok(Some(checked_hooq_toml));
        }

        let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
        let path = PathBuf::from(dir_path).join("hooq.toml");

        if let Ok(false) | Err(_) = path.try_exists() {
            return Ok(None);
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;

        let hooq_toml: HooqToml = toml::from_str(&content)
            .map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;

        let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;

        LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());

        Ok(Some(checked_hooq_toml))
    }

    // NOTE:
    // 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
    // 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
    //
    // 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
    // プロジェクトごとに保存領域を分けるようにした
    fn set(&self, checked_hooq_toml: CheckedHooqToml) {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().insert(key, checked_hooq_toml);
    }

    fn get(&self) -> Option<CheckedHooqToml> {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().get(&key).cloned()
    }
}

impl FlavorStore {
    fn new() -> Self {
        let flavors = presets::preset_flavors();

        Self { flavors }
    }

    pub fn with_hooq_toml() -> Result<Self, String> {
        let mut flavors = Self::new();

        if let Some(hooq_toml) = TomlStore::load()? {
            // 変換が成功することはCheckedHooqTomlの生成時に確認済み
            toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
        }

        Ok(flavors)
    }

    fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
        let mut path = path.iter();
        let mut current: &Flavor = self.flavors.get(path.next()?)?;

        for name in path {
            current = current.sub_flavors.get(name)?;
        }

        Some(current.clone())
    }

    pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
        self.get_flavor_inner(path).ok_or_else(|| {
            format!(
                "flavor `{}` is not found. available flavors:
{}",
                path.join("::"),
                self.all_flavor_names()
                    .into_iter()
                    .map(|name| format!("  - {name}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            )
        })
    }

    pub fn all_flavor_names(&self) -> Vec<String> {
        fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
            let current_name = format!("{prefix}::");

            for (sub_name, sub_flavor) in &flavor.sub_flavors {
                let full_name = format!("{current_name}{sub_name}");
                names.push(full_name.clone());
                collect_names(sub_flavor, &full_name, names);
            }
        }

        let mut names = Vec::new();

        for (name, flavor) in &self.flavors {
            names.push(name.clone());
            collect_names(flavor, name, &mut names);
        }

        names
    }
}

impl TryFrom<HooqToml> for FlavorStore {
    type Error = String;

    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let mut flavors = Self::new();

        toml_load::apply::update_flavors(&mut flavors.flavors, value)?;

        Ok(flavors)
    }
}

impl TryFrom<HooqToml> for CheckedHooqToml {
    type Error = String;

    // ロード時に変換可能かをあらかじめ確認する
    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let _ = FlavorStore::try_from(value.clone())?;

        Ok(Self { inner: value })
    }
}

Default method (Please ignore Japanese comments):

use std::collections::HashMap;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::{LazyLock, Mutex};

use proc_macro2::TokenStream;
use syn::{Expr, Path, parse_quote};

pub use crate::impls::flavor::flavor_path::FlavorPath;
use crate::impls::flavor::toml_load::HooqToml;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::method::Method;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

mod flavor_path;
mod presets;
mod toml_load;

#[derive(Debug, Clone)]
pub struct Flavor {
    pub trait_uses: Vec<Path>,
    pub method: Method,
    pub hook_targets: HookTargetSwitch,
    pub tail_expr_idents: Vec<String>,
    pub ignore_tail_expr_idents: Vec<String>,
    pub result_types: Vec<String>,
    pub hook_in_macros: bool,
    pub bindings: HashMap<String, Rc<Expr>>,
    pub sub_flavors: HashMap<String, Flavor>,
}

impl Default for Flavor {
    fn default() -> Self {
        Self {
            trait_uses: Vec::new(),
            method: default_method(),
            hook_targets: HookTargetSwitch {
                question: true,
                return_: true,
                tail_expr: true,
            },
            tail_expr_idents: vec!["Err".to_string()],
            ignore_tail_expr_idents: vec!["Ok".to_string()],
            result_types: vec!["Result".to_string()],
            hook_in_macros: true,
            bindings: HashMap::new(),
            sub_flavors: HashMap::new(),
        }
    }
}

fn default_method() -> Method {
    // NOTE:
    // $path や $line は eprintln! に直接埋め込みたいところだが、
    // CI側のテストの関係でこのようになっている

    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    let res: TokenStream = parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::std::eprintln!("[{path}:{line}:{col}] {e:?}\n{expr}");
        })
    };

    Method::try_from(res).expect(UNEXPECTED_ERROR_MESSAGE)
}

#[derive(Debug)]
pub struct FlavorStore {
    flavors: HashMap<String, Flavor>,
}

// NOTE:
// 本当はパース後のFlavorsをグローバルに保持したいが、OnceLockはSync制約があるため
// TokenStreamを使っているFlavorsはそのままでは保持できない
// そのため変換確認だけ行った状態でHooqTomlをグローバルに保持し、
// 実際にFlavorsが必要な時にはunwrapを許可することにした
#[derive(Debug, Clone)]
pub struct CheckedHooqToml {
    pub inner: HooqToml,
}

pub struct TomlStore {
    inner: Mutex<HashMap<String, CheckedHooqToml>>,
}

pub static LOADED_HOOQ_TOML: LazyLock<TomlStore> = LazyLock::new(|| TomlStore {
    inner: Mutex::new(HashMap::new()),
});

impl TomlStore {
    fn load() -> Result<Option<CheckedHooqToml>, String> {
        if let Some(checked_hooq_toml) = LOADED_HOOQ_TOML.get() {
            return Ok(Some(checked_hooq_toml));
        }

        let dir_path = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());
        let path = PathBuf::from(dir_path).join("hooq.toml");

        if let Ok(false) | Err(_) = path.try_exists() {
            return Ok(None);
        }

        let content = std::fs::read_to_string(&path)
            .map_err(|e| format!("failed to read file `{}`: {}", path.display(), e))?;

        let hooq_toml: HooqToml = toml::from_str(&content)
            .map_err(|e| format!("failed to parse toml from file `{}`: {}", path.display(), e))?;

        let checked_hooq_toml = CheckedHooqToml::try_from(hooq_toml)?;

        LOADED_HOOQ_TOML.set(checked_hooq_toml.clone());

        Ok(Some(checked_hooq_toml))
    }

    // NOTE:
    // 異なるCargoプロジェクトを同じタイミングでVSCodeで開いていた時に
    // 違うTomlStoreの内容が違うプロジェクトに供給されている事象が見られた
    //
    // 効果がどれぐらいあるかは懐疑的であるが、少しでも軽減すべく
    // プロジェクトごとに保存領域を分けるようにした
    fn set(&self, checked_hooq_toml: CheckedHooqToml) {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().insert(key, checked_hooq_toml);
    }

    fn get(&self) -> Option<CheckedHooqToml> {
        let key = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string());

        self.inner.lock().unwrap().get(&key).cloned()
    }
}

impl FlavorStore {
    fn new() -> Self {
        let flavors = presets::preset_flavors();

        Self { flavors }
    }

    pub fn with_hooq_toml() -> Result<Self, String> {
        let mut flavors = Self::new();

        if let Some(hooq_toml) = TomlStore::load()? {
            // 変換が成功することはCheckedHooqTomlの生成時に確認済み
            toml_load::apply::update_flavors(&mut flavors.flavors, hooq_toml.inner).unwrap();
        }

        Ok(flavors)
    }

    fn get_flavor_inner(&self, path: &FlavorPath) -> Option<Flavor> {
        let mut path = path.iter();
        let mut current: &Flavor = self.flavors.get(path.next()?)?;

        for name in path {
            current = current.sub_flavors.get(name)?;
        }

        Some(current.clone())
    }

    pub fn get_flavor(&self, path: &FlavorPath) -> Result<Flavor, String> {
        self.get_flavor_inner(path).ok_or_else(|| {
            format!(
                "flavor `{}` is not found. available flavors:
{}",
                path.join("::"),
                self.all_flavor_names()
                    .into_iter()
                    .map(|name| format!("  - {name}"))
                    .collect::<Vec<_>>()
                    .join("\n")
            )
        })
    }

    pub fn all_flavor_names(&self) -> Vec<String> {
        fn collect_names(flavor: &Flavor, prefix: &str, names: &mut Vec<String>) {
            let current_name = format!("{prefix}::");

            for (sub_name, sub_flavor) in &flavor.sub_flavors {
                let full_name = format!("{current_name}{sub_name}");
                names.push(full_name.clone());
                collect_names(sub_flavor, &full_name, names);
            }
        }

        let mut names = Vec::new();

        for (name, flavor) in &self.flavors {
            names.push(name.clone());
            collect_names(flavor, name, &mut names);
        }

        names
    }
}

impl TryFrom<HooqToml> for FlavorStore {
    type Error = String;

    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let mut flavors = Self::new();

        toml_load::apply::update_flavors(&mut flavors.flavors, value)?;

        Ok(flavors)
    }
}

impl TryFrom<HooqToml> for CheckedHooqToml {
    type Error = String;

    // ロード時に変換可能かをあらかじめ確認する
    fn try_from(value: HooqToml) -> Result<Self, Self::Error> {
        let _ = FlavorStore::try_from(value.clone())?;

        Ok(Self { inner: value })
    }
}

Usage:

use hooq::hooq;

#[hooq]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    Err("Hello, world!".into())
}

Result:

[mdbook-source-code/flavor-default/src/main.rs:5:5] "Hello, world!"
   5>    Err("Hell..rld!".into())
    |
Error: "Hello, world!"

You can override via hooq.toml.

empty

Disables hooking; intended for conditional builds like #[cfg_attr(feature = "...", hooq(empty))].

use std::collections::HashMap;

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::inert_attr::context::HookTargetSwitch;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn empty_flavor() -> Flavor {
    Flavor {
        trait_uses: Vec::new(),
        method: empty_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        hook_targets: HookTargetSwitch {
            question: false,
            return_: false,
            tail_expr: false,
        },
        tail_expr_idents: Vec::new(),
        ignore_tail_expr_idents: Vec::new(),
        result_types: Vec::new(),
        hook_in_macros: false,
        bindings: HashMap::new(),
        sub_flavors: HashMap::new(),
    }
}

fn empty_method() -> TokenStream {
    parse_quote! {
        $expr
    }
}

Not overridable.

hook

(Please ignore Japanese comments)

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn hook_flavor() -> Flavor {
    Flavor {
        // NOTE: Traitの存在を前提とするflavorだがユーザーが決定する必要あり
        // trait_uses: Vec::new(), // Default と同じ
        method: hook_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn hook_method() -> TokenStream {
    parse_quote! {
        .hook(|| {
            $hooq_meta
        })
    }
}

Designed for user traits to implement a hook method. It is useful when you do not want to use hooq.toml.

Usage:

use hooq::hooq;

mod my_error {
    pub trait MyHook {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
    }

    impl<T, E> MyHook for Result<T, E>
    where
        E: std::fmt::Debug,
    {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
            if let Err(e) = &self {
                let meta = meta_fn();

                eprintln!(
                    "[{}:{}:{}] error occurred: {:?}",
                    meta.file, meta.line, meta.column, e
                );
            }

            self
        }
    }
}

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(hook, trait_uses(my_error::MyHook))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())?;

    Ok(())
}

The second argument is a closure (meta_fn) for lazy evaluation to avoid constructing HooqMeta everywhere.

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod my_error {
    pub trait MyHook {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self;
    }
    impl<T, E> MyHook for Result<T, E>
    where
        E: std::fmt::Debug,
    {
        fn hook(self, meta_fn: impl FnOnce() -> hooq::HooqMeta) -> Self {
            if let Err(e) = &self {
                let meta = meta_fn();
                {
                    ::std::io::_eprint(
                        format_args!(
                            "[{0}:{1}:{2}] error occurred: {3:?}\n",
                            meta.file,
                            meta.line,
                            meta.column,
                            e,
                        ),
                    );
                };
            }
            self
        }
    }
}
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
#[allow(unused)]
use my_error::MyHook as _;
fn main() -> Result<(), Box<dyn std::error::Error>> {
    failable(())
        .hook(|| {
            ::hooq::HooqMeta {
                line: 33usize,
                column: 17usize,
                path: "mdbook-source-code/flavor-hook/src/main.rs",
                file: "main.rs",
                source_str: "failable(()) ?",
                count: "1st ?",
                bindings: ::std::collections::HashMap::from([]),
            }
        })?;
    Ok(())
}

anyhow

Requires anyhow feature (included in default).

This flavor is intended to be used with the anyhow crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn anyhow_flavor() -> Flavor {
    Flavor {
        trait_uses: vec![parse_quote! { ::anyhow::Context }],
        method: anyhow_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn anyhow_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .with_context(|| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            format!("[{path}:{line}:{col}]\n{expr}")
        })
    }
}

Imports anyhow::Context for .with_context(...).

Usage:

use hooq::hooq;

#[hooq(anyhow)]
fn func1() -> anyhow::Result<i32> {
    Err(anyhow::anyhow!("Error in func1"))
}

#[hooq(anyhow)]
fn func2() -> anyhow::Result<i32> {
    let res = func1()?;

    println!("{res}");

    Ok(res)
}

#[hooq(anyhow)]
fn main() -> anyhow::Result<()> {
    func2()?;

    Ok(())
}

Result:

Error: [mdbook-source-code/flavor-anyhow/src/main.rs:19:12]
  19>    func2()?
    |

Caused by:
    0: [mdbook-source-code/flavor-anyhow/src/main.rs:10:22]
         10>    func1()?
           |
    1: [mdbook-source-code/flavor-anyhow/src/main.rs:5:5]
          5>    Err(anyhow::anyhow!("Error in func1"))
           |
    2: Error in func1

eyre

Requires eyre feature (included in default).

This flavor is intended to be used with the eyre crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn eyre_flavor() -> Flavor {
    Flavor {
        trait_uses: vec![parse_quote! { ::eyre::WrapErr }],
        method: eyre_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn eyre_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .wrap_err_with(|| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            format!("[{path}:{line}:{col}]\n{expr}")
        })
    }
}

Imports eyre::WrapErr for .wrap_err_with(...).

Usage:

use hooq::hooq;

#[hooq(eyre)]
fn func1() -> eyre::Result<i32> {
    Err(eyre::eyre!("Error in func1"))
}

#[hooq(eyre)]
fn func2() -> eyre::Result<i32> {
    let res = func1()?;

    println!("{res}");

    Ok(res)
}

#[hooq(eyre)]
fn main() -> eyre::Result<()> {
    func2()?;

    Ok(())
}

Result:

Error: [mdbook-source-code/flavor-eyre/src/main.rs:19:12]
  19>    func2()?
    |

Caused by:
   0: [mdbook-source-code/flavor-eyre/src/main.rs:10:22]
        10>    func1()?
          |
   1: [mdbook-source-code/flavor-eyre/src/main.rs:5:5]
         5>    Err(eyre::eyre!("Error in func1"))
          |
   2: Error in func1

Location:
    mdbook-source-code/flavor-eyre/src/main.rs:5:9

log

Requires log feature (included in default).

This flavor is intended to be used with the log crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn log_flavor() -> Flavor {
    Flavor {
        method: log_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn log_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::excerpted_pretty_stringify!($source);

            ::log::error!("({path}:{line}:{col}) {e}\n{expr}");
        })
    }
}

Usage:

use hooq::hooq;

#[hooq(log)]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    env_logger::Builder::new()
        .format_timestamp(None)
        .filter_level(log::LevelFilter::Error)
        .init();

    Err("Hello, world!".into())
}

Result:

[ERROR flavor_log] (mdbook-source-code/flavor-log/src/main.rs:10:5) Hello, world!
      10>    Err("Hell..rld!".into())
        |
Error: "Hello, world!"

tracing

Requires tracing feature (included in default).

This flavor is intended to be used with the tracing crate.

use proc_macro2::TokenStream;
use syn::parse_quote;

use crate::impls::flavor::Flavor;
use crate::impls::utils::unexpected_error_message::UNEXPECTED_ERROR_MESSAGE;

pub fn tracing_flavor() -> Flavor {
    Flavor {
        method: tracing_method().try_into().expect(UNEXPECTED_ERROR_MESSAGE),
        ..Default::default()
    }
}

fn tracing_method() -> TokenStream {
    let excerpted_helpers_path = crate::impls::utils::get_source_excerpt_helpers_name_space();

    parse_quote! {
        .inspect_err(|e| {
            let path = $path;
            let line = $line;
            let col = $col;
            let expr = #excerpted_helpers_path ::one_line_stringify!($source);

            ::tracing::error!(
                path,
                line,
                col,
                error = %e,
                expr,
            );
        })
    }
}

Place #[hooq(tracing)] above #[tracing::instrument] to ensure order.

Usage:

use hooq::hooq;
use tracing::instrument;

#[hooq(tracing)]
#[instrument]
fn func1() -> Result<i32, String> {
    Err("Error in func1".into())
}

#[hooq(tracing)]
#[instrument]
fn func2() -> Result<i32, String> {
    println!("func2 start");

    let res = func1()?;

    println!("func2 end: {res}");

    Ok(res)
}

#[hooq(tracing)]
#[instrument]
fn func3() -> Result<i32, String> {
    println!("func3 start");

    let res = func2()?;

    println!("func3 end: {res}");

    Ok(res)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let format = tracing_subscriber::fmt::format()
        .with_ansi(false)
        .without_time();
    tracing_subscriber::fmt().event_format(format).init();

    func3()?;

    Ok(())
}

Result:

func3 start
func2 start
ERROR func3:func2:func1: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=7 col=5 error=Error in func1 expr="Err(\"Error in func1\".into())"
ERROR func3:func2: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=15 col=22 error=Error in func1 expr="func1()?"
ERROR func3: flavor_tracing: path="mdbook-source-code/flavor-tracing/src/main.rs" line=27 col=22 error=Error in func1 expr="func2()?"

Error: "Error in func1"

Features

The hooq crate provides several Cargo features:

FeaturedefaultDescription
defaultDefault set; includes all except consume-question.
fullIncludes all features.
anyhowProvides the anyhow flavor.
eyreProvides the eyre flavor.
logProvides the log flavor.
tracingProvides the tracing flavor.
consume-questionAllows removing the ? operator via a trailing ! on hooked methods.

Only consume-question is excluded from default. Flavor‑related features are included by default.

Recipes & Ideas

This chapter collects smaller ideas enabled by hooq.

Practical usage has already been shown in the Tutorial. These entries may be less practical but explore possibilities and might inspire new use cases.

Contents

PageSummary
Perfect color-eyreUse color-eyre for BACKTRACE/SPANTRACE, while also getting hooq’s pseudo trace for maximum insight.
Batch apply under a modulehooq can be attached to items like mod to hook inner functions recursively.
Using in functions returning OptionAdjust tail_expr_idents and result_types to target Option returns.
Turn ? into .unwrap() behind a featureUse #[cfg_attr(...)] to change behavior conditionally.
Reinvent match desugaringReplace ? with match using consume-question + custom flavor.

Perfect color-eyre

color-eyre provides advanced ways to obtain BACKTRACE and SPANTRACE and is hard to ignore.

Let’s extend the official example by adding hooq’s pseudo trace.

Create a new project and set the following in Cargo.toml (replace hooq with the latest version):

[features]
default = ["capture-spantrace"]
capture-spantrace = []

[dependencies]
color-eyre = "0.6.5"
hooq = { path = "../../hooq" } # Please rewrite `hooq = "*"`
tracing = "0.1.43"
tracing-error = "0.2.1"
tracing-subscriber = { version = "0.3.22", features = ["env-filter"] }

We want color_eyre::eyre::WrapErr instead of ::eyre::WrapErr, so place hooq.toml next to Cargo.toml to override the import. Other settings inherit from the built‑in eyre flavor.

[eyre]
trait_uses = ["color_eyre::eyre::WrapErr"]

Write main.rs. use hooq::hooq; and annotate #[hooq(eyre)] above #[instrument] (we also add .without_time() to stabilize snapshots).

use color_eyre::Section;
use color_eyre::eyre::{Report, WrapErr};
use hooq::hooq;
use tracing::{info, instrument};

#[hooq(eyre)]
#[instrument]
fn main() -> Result<(), Report> {
    #[cfg(feature = "capture-spantrace")]
    install_tracing();

    color_eyre::install()?;

    read_config()
}

#[cfg(feature = "capture-spantrace")]
fn install_tracing() {
    use tracing_error::ErrorLayer;
    use tracing_subscriber::prelude::*;
    use tracing_subscriber::{EnvFilter, fmt};

    let fmt_layer = fmt::layer().with_target(false).without_time();
    let filter_layer = EnvFilter::try_from_default_env()
        .or_else(|_| EnvFilter::try_new("info"))
        .unwrap();

    tracing_subscriber::registry()
        .with(filter_layer)
        .with(fmt_layer)
        .with(ErrorLayer::default())
        .init();
}

#[hooq(eyre)]
#[instrument]
fn read_file(path: &str) -> Result<(), Report> {
    info!("Reading file");
    std::fs::read_to_string(path).map(drop)
}

#[hooq(eyre)]
#[instrument]
fn read_config() -> Result<(), Report> {
    read_file("fake_file")
        .wrap_err("Unable to read config")
        .suggestion("try using a file that exists next time")
}

Run with RUST_LIB_BACKTRACE=1 to also see the BACKTRACE:

Error: 
   0: [mdbook-source-code/recipe-color-eyre/src/main.rs:14:5]
        14>    read_config()
          |
   1: [mdbook-source-code/recipe-color-eyre/src/main.rs:45:5]
        45>    read_file("fake_file")
        46|        .wrap_err("Unab..nfig")
        47|        .suggestion("try ..time")
          |
   2: Unable to read config
   3: [mdbook-source-code/recipe-color-eyre/src/main.rs:39:5]
        39>    std::fs::read_to_string(path).map(drop)
          |
   4: No such file or directory (os error 2)

Location:
   mdbook-source-code/recipe-color-eyre/src/main.rs:39

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ SPANTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

   0: recipe_color_eyre::read_file with path="fake_file"
      at mdbook-source-code/recipe-color-eyre/src/main.rs:36
   1: recipe_color_eyre::read_config
      at mdbook-source-code/recipe-color-eyre/src/main.rs:43

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ BACKTRACE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
                                ⋮ 4 frames hidden ⋮                               
   5: <E as eyre::context::ext::StdError>::ext_report::h0e9fcf40b567f05e
      at /home/USER/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/eyre-0.6.12/src/context.rs:26
                                 ⋮ 1 frame hidden ⋮                               
   7: recipe_color_eyre::read_file::h74782b4bb409e5a9
      at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:39
   8: recipe_color_eyre::read_config::h8139da57eb93337e
      at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:45
   9: recipe_color_eyre::main::h9c27f45de169a3be
      at /home/USER/workspace/hooq/mdbook-source-code/recipe-color-eyre/src/main.rs:14
  10: core::ops::function::FnOnce::call_once::h724113526cc98cab
      at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:250
  11: std::sys::backtrace::__rust_begin_short_backtrace::h9e20a34286867e4d
      at /home/USER/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/std/src/sys/backtrace.rs:158
                                ⋮ 14 frames hidden ⋮                              

Suggestion: try using a file that exists next time

Run with COLORBT_SHOW_HIDDEN=1 environment variable to disable frame filtering.
Run with RUST_BACKTRACE=full to include source snippets.

This is likely the most detailed output.

Batch Apply Under a Module

hooq can be attached not only to functions but to items containing executable syntax.

Here, an “item” refers to Rust syntax elements that can appear directly inside a module/scope: functions, mod, impl blocks, trait blocks, etc. (See syn::Item.) Items without executable syntax are irrelevant.

When a mod { ... } (brace form, not mod xxx;) is annotated with #[hooq], inner functions can be hooked without annotating each function.

This recursive behavior is not limited to mod. If functions/closures nest, hooq will greedily hook according to settings for inner functions/closures as well. Use #[hooq::skip_all] if compilation fails or you want to disable in a region.

Puzzle example below illustrating which inner values get hooked:

use hooq::hooq;

#[hooq] // attribute macro root exist only here
#[hooq::method(.inspect_err(|_| {}))]
mod hoge {
    pub fn bar() -> i32 {
        // NOT hook target because this function does not return Result
        42
    }

    pub fn failable<T>(val: T) -> Result<T, ()> {
        // The return type of this function is Result
        // but "Ok" is specified as ignore_tail_expr_idents by default
        // so NOT hook target
        Ok(val)
    }

    #[hooq::tail_expr_idents("Ok", "Err")]
    pub fn _failable_2<T>(val: T) -> Result<T, ()> {
        if failable(false).unwrap() {
            // ↑ above is NOT hook target
            // ↓ below: hook target because the return type of this function is Result
            return failable(val);
        }

        // hook target because "Ok" is now specified as tail_expr_idents
        Ok(val)
    }

    pub fn fuga() -> Result<(), ()> {
        // hook target because of `?`
        failable(())?;

        let _ = || {
            // hook target because of `?`
            failable(())?;

            // hook target because of `?`
            if failable(false)? {
                // hook target because "Err" is specified as tail_expr_idents by default
                return Err(());
            }

            // NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
            Ok(())
        };

        let _ = || {
            // NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
            Result::<(), ()>::Ok(())
        };

        let _ = {
            let _ = bar();

            // hook target because "Err" is specified as tail_expr_idents by default
            // even if this is the tail expression of the block not a closure
            Result::<(), ()>::Err(())
        };

        let _ = || -> Result<(), ()> {
            // hook target because hooq can know this closure returns Result
            failable(())
        };

        let _ = || {
            // NOT hook target because hooq cannot know this closure returns Result
            failable(())
        };

        // NOT hook target because "Ok" is specified as ignore_tail_expr_idents by default
        Ok(())
    }
}

fn main() {
    let _ = hoge::bar();
    let _ = hoge::failable(123);
    let _ = hoge::fuga();
}

Expansion shows aggressive yet fairly natural hooking for a macro1:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
mod hoge {
    pub fn bar() -> i32 {
        42
    }
    pub fn failable<T>(val: T) -> Result<T, ()> {
        Ok(val)
    }
    pub fn _failable_2<T>(val: T) -> Result<T, ()> {
        if failable(false).unwrap() {
            return failable(val).inspect_err(|_| {});
        }
        Ok(val).inspect_err(|_| {})
    }
    pub fn fuga() -> Result<(), ()> {
        failable(()).inspect_err(|_| {})?;
        let _ = || {
            failable(()).inspect_err(|_| {})?;
            if failable(false).inspect_err(|_| {})? {
                return Err(()).inspect_err(|_| {});
            }
            Ok(())
        };
        let _ = || { Result::<(), ()>::Ok(()) };
        let _ = {
            let _ = bar();
            Result::<(), ()>::Err(()).inspect_err(|_| {})
        };
        let _ = || -> Result<(), ()> { failable(()).inspect_err(|_| {}) };
        let _ = || { failable(()) };
        Ok(())
    }
}
fn main() {
    let _ = hoge::bar();
    let _ = hoge::failable(123);
    let _ = hoge::fuga();
}

  1. Procedural macros lack rich reflection; we cannot type‑infer Result.

Using in Functions Returning Option

We previously introduced controlling hook target decisions via attributes, which may feel abstract. Here is a more concrete scenario: target only Option values.

In the example below, we hook functions defined inside main (nesting is supported; see Batch apply). We want to inspect when a value is Some.

  • tail_expr_idents: set to Some to hook only Some.
  • ignore_tail_expr_idents: set to None to avoid wasteful hooks.
  • result_types: set to Option so tail/return values are considered.
use std::fmt::Debug;

use hooq::hooq;

#[hooq]
#[hooq::method(.my_inspect())]
#[hooq::tail_expr_idents("Some")]
#[hooq::ignore_tail_expr_idents("None")]
#[hooq::result_types("Option")]
fn main() {
    fn option_fn_1() -> Option<i32> {
        // hook target
        Some(42)
    }

    fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
        // hook target
        let _ = option_fn_1()?;

        // hook target because the return type of the function is Option
        if flag {
            // hook target
            Some(val)
        } else {
            // NOT hook target
            None
        }
    }

    fn result_fn_1() -> Result<i32, ()> {
        // NOT hook target because the return type of the function is Result not Option
        Ok(42)
    }

    fn result_fn_2() -> Result<i32, ()> {
        // HOOK TARGET because of `?`
        // so, #[hooq::skip_all] is needed
        #[hooq::skip_all]
        let _ = result_fn_1()?;

        // NOT hook target because the return type of the function is Result not Option
        Ok(42)
    }

    let _ = option_fn_1();
    let _ = option_fn_2(true, 123);
    let _ = result_fn_1();
    let _ = result_fn_2();
}

trait MyInspect {
    fn my_inspect(self) -> Self;
}

impl<T> MyInspect for Option<T>
where
    T: Debug,
{
    fn my_inspect(self) -> Self {
        match self {
            Some(val) => {
                println!("Inspecting value: {:?}", val);
                Some(val)
            }
            None => None,
        }
    }
}

Expansion:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use std::fmt::Debug;
use hooq::hooq;
fn main() {
    fn option_fn_1() -> Option<i32> {
        Some(42).my_inspect()
    }
    fn option_fn_2<T: Debug>(flag: bool, val: T) -> Option<T> {
        let _ = option_fn_1().my_inspect()?;
        if flag { Some(val).my_inspect() } else { None }.my_inspect()
    }
    fn result_fn_1() -> Result<i32, ()> {
        Ok(42)
    }
    fn result_fn_2() -> Result<i32, ()> {
        let _ = result_fn_1()?;
        Ok(42)
    }
    let _ = option_fn_1();
    let _ = option_fn_2(true, 123);
    let _ = result_fn_1();
    let _ = result_fn_2();
}
trait MyInspect {
    fn my_inspect(self) -> Self;
}
impl<T> MyInspect for Option<T>
where
    T: Debug,
{
    fn my_inspect(self) -> Self {
        match self {
            Some(val) => {
                {
                    ::std::io::_print(format_args!("Inspecting value: {0:?}\n", val));
                };
                Some(val)
            }
            None => None,
        }
    }
}

The point: while Result/Ok/Err are common defaults, hooq does not hard‑code these; you can target other types/idents.

Potentially useful once Try trait stabilizes.

Turn ? into .unwrap() Behind a Feature

As explained in Method, with the consume-question feature enabled, appending ! to the method allows consuming ?.

We can use this to treat ? as an alias for .unwrap().

Example: when an unwrap feature is enabled, replace ? with .unwrap(). Cargo.toml features:

[dependencies]
hooq = { version = "*", features = ["consume-question"] }

# ..

[features]
unwrap = []

Prepare a flavor in hooq.toml. We restrict hook_targets to only ? so tails/returns are unaffected.

[unwrap]
method = """.unwrap()!"""
hook_targets = ["?"]

main.rs:

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn process(flag: bool) -> Result<(), String> {
    if flag {
        return Err("An error occurred".into());
    }

    let _ = failable(42)?;

    Ok(())
}

#[cfg_attr(not(feature = "unwrap"), hooq(empty))]
#[cfg_attr(feature = "unwrap", hooq(unwrap))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
    process(false)?;

    Ok(())
}

Without the unwrap feature, expansion uses the empty flavor and nothing changes:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
    if flag {
        return Err("An error occurred".into());
    }
    let _ = failable(42)?;
    Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    process(false)?;
    Ok(())
}

With the unwrap feature, expansion replaces with .unwrap():

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
fn process(flag: bool) -> Result<(), String> {
    if flag {
        return Err("An error occurred".into());
    }
    let _ = failable(42).unwrap();
    Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
    process(false).unwrap();
    Ok(())
}

Highlights:

  • You can replace ? with special behaviors like .unwrap().
  • Feature‑gated #[cfg_attr(..., hooq(...))] lets you vary hooks by build configuration.

Reinvent match Desugaring

In Turn ? into .unwrap() behind a feature we showed that with the consume-question feature enabled, adding ! at the end of the method allows replacing ?.

We can therefore replace ? with a match. A fun example worth documenting.

First, enable consume-question:

cargo add hooq --features consume-question

Define the match expression in hooq.toml (the original is $expr). The flavor is named my_match since match is reserved:

[my_match]
method = """
match $expr {
    Ok(val) => val,
    Err(err) => return Err(From::from(err)),
}!
"""

Write main.rs as usual (add #[allow(clippy::question_mark)] to avoid lints):

use hooq::hooq;

fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}

#[hooq(my_match)]
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
    let _ = failable(42)?;

    Ok(())
}

Expansion shows a match as intended:

#![feature(prelude_import)]
#[macro_use]
extern crate std;
#[prelude_import]
use std::prelude::rust_2024::*;
use hooq::hooq;
fn failable<T>(val: T) -> Result<T, String> {
    Ok(val)
}
#[allow(clippy::question_mark)]
fn main() -> Result<(), String> {
    let _ = match failable(42) {
        Ok(val) => val,
        Err(err) => return Err(From::from(err)),
    };
    Ok(())
}