How to re-throw any error with `?`

What exactly did you test? Static vtable is better because program is not spending time building anything at runtime, all it does is takes Box<ConcreteType> and turns it into (&'static VTable, Box<ConcreteType>). My testing clearly shows that Box is the problem and it is what should be avoided, not dyn. Another approach: here what happens if you replace String in original code with &'static str and, more importantly, Box<dyn Error> with &'static dyn Error: playground

Release:

Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)
Elements : 10000, Iterations: 1000
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
&dyn Error  β€” mixed ok/err                         |     12.068 ms |    1.207 ns/op
Enum AppError   β€” mixed ok/err                     |     11.514 ms |    1.151 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
&dyn Error  β€” all errors                           |     10.274 ms |    1.027 ns/op
Enum AppError   β€” all errors                       |     10.262 ms |    1.026 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
&dyn Error  β€” all ok                               |     13.161 ms |    1.316 ns/op
Enum AppError   β€” all ok                           |     13.240 ms |    1.324 ns/op

================================================================================
Vtable overhead &dyn Error vs Enum:
  Mixed ok/err  : +4.81%
  All errors    : +0.12%
  All ok        : -0.60%
================================================================================

All overhead is suddenly gone.
And +5% with this benchmark is nothing. I can also get this by just rerunning:

Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)
Elements : 10000, Iterations: 1000
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
&dyn Error  β€” mixed ok/err                         |     11.617 ms |    1.162 ns/op
Enum AppError   β€” mixed ok/err                     |     11.431 ms |    1.143 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
&dyn Error  β€” all errors                           |     10.140 ms |    1.014 ns/op
Enum AppError   β€” all errors                       |     10.145 ms |    1.015 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
&dyn Error  β€” all ok                               |     13.156 ms |    1.316 ns/op
Enum AppError   β€” all ok                           |     13.179 ms |    1.318 ns/op

================================================================================
Vtable overhead &dyn Error vs Enum:
  Mixed ok/err  : +1.63%
  All errors    : -0.05%
  All ok        : -0.17%
================================================================================

In other words, if you replace Box<dyn Error> with Box<AppError> you get same performance degradation as if you continued boxing dyn. And if you replace Box<dyn Error> with &'static dyn Error you get no performance degradation. Clearly, Box is at fault, not dyn.

Generally speaking you should profile your application. Box here is at fault most likely because it inhibits some optimizations, not sure which – it definitely does not have noticeable effect on debug builds. Your application can have something else inhibiting them too: perhaps another Box inside Ok.

But if you don’t want to spend time profiling then custom error types (which thiserror helps you create) would be safer bet.

And for libraries thiserror is always better as it gives more options to the users.

Thanks for your explanations !

Perfect, in this case I correct the chapter about managing exceptions in Rust as

  • if all exceptions in your function are homogeneous, then use .. -> Result<_, named_exception> { .
  • if there are heterogeneous exceptions, then use `... -> Result<_, Box<dyn Error>> {.

It will guarantee a minimum performance degradation. In some cases you can also use map_err when a different exception can happen in few cases.

You raised a performance issue with Box but, finally, it seems to me that your example doesn't reply to my original question : "how to transfert an error with ?".

So I adjust your code like below to be more in situation :

use std::fmt;
use std::time::Instant;

#[derive(Debug)]
enum InternalError {
    AnError(String),
}
impl std::error::Error for InternalError {}

#[derive(Debug)]
enum ExternalError {
    AnError(InternalError),
}
impl std::error::Error for ExternalError {}

fn try_mod_3(input: i32) -> Result<i32, InternalError> {
    if input % 3 == 0 {
        Err(InternalError::AnError(format!("Div by zero")))
    } else {
        Ok(input)
    }
}

fn process_dynamic(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    let i = try_mod_3(input)?;
    Ok(i + 1)
}

fn process_static(input: i32) -> Result<i32, ExternalError> {
    match try_mod_3(input) {
        Ok(o) => Ok(o + 1),
        Err(err) => Err(ExternalError::AnError(err)),
    }
}

impl fmt::Display for InternalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            InternalError::AnError(msg) => write!(f, "An internal error: {}", msg),
        }
    }
}

impl fmt::Display for ExternalError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ExternalError::AnError(msg) => write!(f, "An external error: {}", msg),
        }
    }
}

fn bench_dynamic_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_dynamic(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_static_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_static(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_dynamic_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_dynamic(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_static_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_static(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_dynamic_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_dynamic(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn bench_static_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_static(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn print_result(label: &str, elapsed: std::time::Duration, iterations: u32, n: usize) {
    let total_ops = iterations as u64 * n as u64;
    let ns_per_op = elapsed.as_nanos() as f64 / total_ops as f64;
    println!(
        "{:<50} | {:>10.3} ms | {:>8.3} ns/op",
        label,
        elapsed.as_secs_f64() * 1000.0,
        ns_per_op
    );
}

fn print_overhead(label: &str, dyn_d: std::time::Duration, stat_d: std::time::Duration) {
    let overhead =
        ((dyn_d.as_nanos() as f64 - stat_d.as_nanos() as f64) / stat_d.as_nanos() as f64) * 100.0;
    println!("  {label}: {overhead:+.2}%");
}

fn main() {
    let n = 10_000;
    let iterations = 1_000;

    println!("Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)");
    println!("Elements : {n}, Iterations: {iterations}");
    println!("{}", "=".repeat(80));

    println!("\n[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)");
    println!("{}", "-".repeat(80));
    let d_mix = bench_dynamic_mixed(n, iterations);
    print_result("Box<dyn Error>  β€” mixed ok/err", d_mix, iterations, n);
    let s_mix = bench_static_mixed(n, iterations);
    print_result("Enum AppError   β€” mixed ok/err", s_mix, iterations, n);

    println!("\n[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)");
    println!("{}", "-".repeat(80));
    let d_err = bench_dynamic_all_err(n, iterations);
    print_result("Box<dyn Error>  β€” all errors", d_err, iterations, n);
    let s_err = bench_static_all_err(n, iterations);
    print_result("Enum AppError   β€” all errors", s_err, iterations, n);

    println!("\n[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)");
    println!("{}", "-".repeat(80));
    let d_ok = bench_dynamic_no_err(n, iterations);
    print_result("Box<dyn Error>  β€” all ok", d_ok, iterations, n);
    let s_ok = bench_static_no_err(n, iterations);
    print_result("Enum AppError   β€” all ok", s_ok, iterations, n);

    println!("\n{}", "=".repeat(80));
    println!("Vtable overhead Box<dyn Error> vs Enum:");
    print_overhead("Mixed ok/err  ", d_mix, s_mix);
    print_overhead("All errors    ", d_err, s_err);
    print_overhead("All ok        ", d_ok, s_ok);
    println!("{}", "=".repeat(80));
}

Here the difference about All ok disappear :

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” mixed ok/err                     |      9.237 ms |    0.924 ns/op
Enum AppError   β€” mixed ok/err                     |      4.980 ms |    0.498 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all errors                       |     19.176 ms |    1.918 ns/op
Enum AppError   β€” all errors                       |     10.665 ms |    1.066 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all ok                           |      4.295 ms |    0.430 ns/op
Enum AppError   β€” all ok                           |      4.297 ms |    0.430 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +85.48%
  All errors    : +79.81%
  All ok        : -0.03%
================================================================================

And with some compilation optimization the difference between Box and Enum disappear :

[profile.release]
opt-level = 3
lto = "fat"
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” mixed ok/err                     |      5.215 ms |    0.522 ns/op
Enum AppError   β€” mixed ok/err                     |      5.210 ms |    0.521 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all errors                       |      3.190 ms |    0.319 ns/op
Enum AppError   β€” all errors                       |      3.201 ms |    0.320 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all ok                           |      4.693 ms |    0.469 ns/op
Enum AppError   β€” all ok                           |      4.737 ms |    0.474 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +0.10%
  All errors    : -0.33%
  All ok        : -0.94%
================================================================================

If I didn't make any mistake --> Box is not so bad (and like @MOCKBA said t isn't my concern since an exception happens quite rarely)

I commented about the effect of Box<dyn> in case you don't know, so you can really choose what you want. Pure enum or Box<dyn>

You changed your code to use enum, notice the method try_mod_3 uses enum not Box<dyn>. I changed it back to using Box<dyn> and reverted back the total variant to be more than 1 because in your code there is only 1 error variant where in real app has many error variants, and the slow result comes again

The code :

use std::fmt;
use std::time::Instant;

#[derive(Debug)]
enum AppError {
    AnError(String),
    Error2(String),
}
impl std::error::Error for AppError {}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::AnError(msg) => write!(f, "An error: {}", msg),
            AppError::Error2(msg) => write!(f, "An error 2: {}", msg),
        }
    }
}

fn try_mod_3(input: i32) -> Result<i32, AppError> {
    match input % 3 {
        0 => Err(AppError::AnError(format!("Div by zero: {}", input))),
        1 => Err(AppError::Error2(format!("unexpected: {}", input))),
        _ => Ok(input + 2),
    }
}

fn try_mod_dyn(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    match input % 3 {
        0 => Err(Box::new(AppError::AnError(format!("Div by zero: {}", input)))),
        1 => Err(Box::new(AppError::Error2(format!("unexpected: {}", input)))),
        _ => Ok(input + 2),
    }
}

fn process_dynamic(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    let i = try_mod_dyn(input)?;
    Ok(i + 1)
}

fn process_static(input: i32) -> Result<i32, AppError> {
    let i = try_mod_3(input)?;
    Ok(i + 1)
}

fn bench_dynamic_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_dynamic(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_static_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_static(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_dynamic_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_dynamic(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_static_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_static(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_dynamic_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_dynamic(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn bench_static_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_static(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn print_result(label: &str, elapsed: std::time::Duration, iterations: u32, n: usize) {
    let total_ops = iterations as u64 * n as u64;
    let ns_per_op = elapsed.as_nanos() as f64 / total_ops as f64;
    println!(
        "{:<50} | {:>10.3} ms | {:>8.3} ns/op",
        label,
        elapsed.as_secs_f64() * 1000.0,
        ns_per_op
    );
}

fn print_overhead(label: &str, dyn_d: std::time::Duration, stat_d: std::time::Duration) {
    let overhead =
        ((dyn_d.as_nanos() as f64 - stat_d.as_nanos() as f64) / stat_d.as_nanos() as f64) * 100.0;
    println!("  {label}: {overhead:+.2}%");
}

fn main() {
    let n = 10_000;
    let iterations = 1_000;

    println!("Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)");
    println!("Elements : {n}, Iterations: {iterations}");
    println!("{}", "=".repeat(80));

    println!("\n[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)");
    println!("{}", "-".repeat(80));
    let d_mix = bench_dynamic_mixed(n, iterations);
    print_result("Box<dyn Error>  β€” mixed ok/err", d_mix, iterations, n);
    let s_mix = bench_static_mixed(n, iterations);
    print_result("Enum AppError   β€” mixed ok/err", s_mix, iterations, n);

    println!("\n[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)");
    println!("{}", "-".repeat(80));
    let d_err = bench_dynamic_all_err(n, iterations);
    print_result("Box<dyn Error>  β€” all errors", d_err, iterations, n);
    let s_err = bench_static_all_err(n, iterations);
    print_result("Enum AppError   β€” all errors", s_err, iterations, n);

    println!("\n[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)");
    println!("{}", "-".repeat(80));
    let d_ok = bench_dynamic_no_err(n, iterations);
    print_result("Box<dyn Error>  β€” all ok", d_ok, iterations, n);
    let s_ok = bench_static_no_err(n, iterations);
    print_result("Enum AppError   β€” all ok", s_ok, iterations, n);

    println!("\n{}", "=".repeat(80));
    println!("Vtable overhead Box<dyn Error> vs Enum:");
    print_overhead("Mixed ok/err  ", d_mix, s_mix);
    print_overhead("All errors    ", d_err, s_err);
    print_overhead("All ok        ", d_ok, s_ok);
    println!("{}", "=".repeat(80));
}

The result :

Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)
Elements : 10000, Iterations: 1000
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” mixed ok/err                     |   1002.715 ms |  100.271 ns/op
Enum AppError   β€” mixed ok/err                     |    505.374 ms |   50.537 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all errors                       |   1041.453 ms |  104.145 ns/op
Enum AppError   β€” all errors                       |    704.759 ms |   70.476 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all ok                           |     33.374 ms |    3.337 ns/op
Enum AppError   β€” all ok                           |     15.659 ms |    1.566 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +98.41%
  All errors    : +47.77%
  All ok        : +113.13%

Yes I didn't know, thanks for it :+1:

Here your code doesn't measure the "throw" of the error, it measures the "creation"+"throw". It's why I put the same "creation" code for both : for testing the time of the "throw" only .

With 2 variants + optimized options + same creation , there's indeed a small slowdown, but only for error cases :

Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)
Elements : 10000, Iterations: 1000
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” mixed ok/err                     |    330.941 ms |   33.094 ns/op
Enum AppError   β€” mixed ok/err                     |    290.734 ms |   29.073 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all errors                       |    500.187 ms |   50.019 ns/op
Enum AppError   β€” all errors                       |    439.445 ms |   43.944 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all ok                           |     15.492 ms |    1.549 ns/op
Enum AppError   β€” all ok                           |     15.522 ms |    1.552 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +13.83%
  All errors    : +13.82%
  All ok        : -0.19%
================================================================================

Because in real usage does not just bubble up existing error from 3rd party libraries, but also creates the app's own error. Unless all your errors are just errors originating from 3rd libraries

Because in real usage, to get a useful error message, it is not just a re return of the plain error from the 3rd library. But you add more detailed info/context based on your app, like user id, thus creating error, thus you pay Box<dyn> cost. For example

use std::fmt;
use std::time::Instant;

#[derive(Debug)]
enum AppError {
    AnError(String),
    Error2(String),
}
impl std::error::Error for AppError {}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::AnError(msg) => write!(f, "An error: {}", msg),
            AppError::Error2(msg) => write!(f, "An error 2: {}", msg),
        }
    }
}

impl AppError {
    fn add_context(self, ctx: &str) -> Self {
        match self {
            AppError::AnError(msg) => AppError::AnError(format!("{ctx}: {msg}")),
            AppError::Error2(msg) => AppError::Error2(format!("{ctx}: {msg}")),
        }
    }
}

fn try_mod_3(input: i32) -> Result<i32, AppError> {
    match input % 3 {
        0 => Err(AppError::AnError(format!("Div by zero: {}", input))),
        1 => Err(AppError::Error2(format!("unexpected: {}", input))),
        _ => Ok(input + 2),
    }
}

fn process_dynamic_first(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    let i = try_mod_3(input).map_err(|e| e.add_context("process_dynamic_first"))?;
    Ok(i + 1)
}

fn process_dynamic(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    let i = process_dynamic_first(input).map_err(|e| format!("process_dynamic: {e}"))?;
    Ok(i + 1)
}

fn process_static_first(input: i32) -> Result<i32, AppError> {
    let i = try_mod_3(input).map_err(|e| e.add_context("process_static_first"))?;
    Ok(i + 1)
}

fn process_static(input: i32) -> Result<i32, AppError> {
    let i = process_static_first(input).map_err(|e| e.add_context("process_static"))?;
    Ok(i + 1)
}

fn bench_dynamic_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_dynamic(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_static_mixed(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            match process_static(v) {
                Ok(_) => ok_count += 1,
                Err(_) => err_count += 1,
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box((ok_count, err_count));
    elapsed
}

fn bench_dynamic_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_dynamic(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_static_all_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3).collect();
    let start = Instant::now();
    let mut err_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if process_static(v).is_err() {
                err_count += 1;
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(err_count);
    elapsed
}

fn bench_dynamic_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_dynamic(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn bench_static_no_err(n: usize, iterations: u32) -> std::time::Duration {
    let inputs: Vec<i32> = (0..n as i32).map(|i| i * 3 + 2).collect();
    let start = Instant::now();
    let mut ok_count = 0usize;
    for _ in 0..iterations {
        for &v in &inputs {
            if let Ok(x) = process_static(v) {
                ok_count += 1;
                std::hint::black_box(x);
            }
        }
    }
    let elapsed = start.elapsed();
    std::hint::black_box(ok_count);
    elapsed
}

fn print_result(label: &str, elapsed: std::time::Duration, iterations: u32, n: usize) {
    let total_ops = iterations as u64 * n as u64;
    let ns_per_op = elapsed.as_nanos() as f64 / total_ops as f64;
    println!(
        "{:<50} | {:>10.3} ms | {:>8.3} ns/op",
        label,
        elapsed.as_secs_f64() * 1000.0,
        ns_per_op
    );
}

fn print_overhead(label: &str, dyn_d: std::time::Duration, stat_d: std::time::Duration) {
    let overhead =
        ((dyn_d.as_nanos() as f64 - stat_d.as_nanos() as f64) / stat_d.as_nanos() as f64) * 100.0;
    println!("  {label}: {overhead:+.2}%");
}

fn pin_to_core(core_id: usize) {
    let cores = core_affinity::get_core_ids().expect("gagal ambil core ids");
    let core = cores.get(core_id).expect("core id tidak ditemukan");
    core_affinity::set_for_current(*core);
}

fn main() {

    pin_to_core(0);

    let n = 10_000;
    let iterations = 1_000;

    println!("Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)");
    println!("Elements : {n}, Iterations: {iterations}");
    println!("{}", "=".repeat(80));

    println!("\n[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)");
    println!("{}", "-".repeat(80));
    let d_mix = bench_dynamic_mixed(n, iterations);
    print_result("Box<dyn Error>  β€” mixed ok/err", d_mix, iterations, n);
    let s_mix = bench_static_mixed(n, iterations);
    print_result("Enum AppError   β€” mixed ok/err", s_mix, iterations, n);

    println!("\n[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)");
    println!("{}", "-".repeat(80));
    let d_err = bench_dynamic_all_err(n, iterations);
    print_result("Box<dyn Error>  β€” all errors", d_err, iterations, n);
    let s_err = bench_static_all_err(n, iterations);
    print_result("Enum AppError   β€” all errors", s_err, iterations, n);

    println!("\n[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)");
    println!("{}", "-".repeat(80));
    let d_ok = bench_dynamic_no_err(n, iterations);
    print_result("Box<dyn Error>  β€” all ok", d_ok, iterations, n);
    let s_ok = bench_static_no_err(n, iterations);
    print_result("Enum AppError   β€” all ok", s_ok, iterations, n);

    println!("\n{}", "=".repeat(80));
    println!("Vtable overhead Box<dyn Error> vs Enum:");
    print_overhead("Mixed ok/err  ", d_mix, s_mix);
    print_overhead("All errors    ", d_err, s_err);
    print_overhead("All ok        ", d_ok, s_ok);
    println!("{}", "=".repeat(80));
}

That benchmark still runs on rust playground because it is reachable for fast testing. But now, there is more thing added, so needed local run to get more stable result because rust playground's CPU is shared with all users. And added CPU core pin. After that, the Box<dyn> is always slower in all scenarios, no exception, no more noise

Benchmark: Error Type β€” Box<dyn Error> (vtable) vs Enum (non-vtable)
Elements : 10000, Iterations: 1000
================================================================================

[Skenario 1] Mixed Ok/Err (~33% Ok, ~67% Err)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” mixed ok/err                     |  12131.635 ms | 1213.163 ns/op
Enum AppError   β€” mixed ok/err                     |  11085.934 ms | 1108.593 ns/op

[Skenario 2] All Errors (100% Err path β€” heap alloc setiap iterasi)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all errors                       |  18351.856 ms | 1835.186 ns/op
Enum AppError   β€” all errors                       |  16877.655 ms | 1687.766 ns/op

[Skenario 3] All Ok (100% happy path β€” error type tidak pernah dibuat)
--------------------------------------------------------------------------------
Box<dyn Error>  β€” all ok                           |    210.047 ms |   21.005 ns/op
Enum AppError   β€” all ok                           |     90.949 ms |    9.095 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +9.43%
  All errors    : +8.73%
  All ok        : +130.95%

Yes, ok. So if I understand the good practice for large project is to use Enum to manage errors.
Thanks for your clarification.

Yep. Another one is, to be able to use ?, you are either use enum + impl from (with library for syntax sugar), you get zero cost error handling, error handling becomes like static control flow. Or Box<dyn>, but you have to put it in any function that you want to use ?, you can not do like this

fn a -> fn b -> fn c

Only put Box<dyn> in fn a, and expect to be able to use the sugar ? in fn b and c. All places that you want to use ? need Box<dyn> if you want to use Box<dyn>. And then creating error is not just when the first time the error created by 3rd library, but when you add your own custom detailed info/context it is also creating error, more specifically recreating the error

There is also a popular joke in other high performance language communities that Rust code does not always have equal speed to C/C++/Zig due to how its user writes it, like this one. To get equal speed to C/C++/Zig etc, you have to use the zero cost approach

Yes, it makes sense

What do you mean by zero cost approach in the case of Arc<Mutex<Box<dyn T>>> ?

Are you sure that you haven't mixed the Release measurements with the Debug measurements? Or should the numbers show the speedup? It looks quite strange that a release build is much slower then the corresponding debug build.

All measurements are relative to enum case within the same build. It is expected that without optimization debug builds are unaffected when no errors are generated while release builds suffer from (presumably) missed optimization.