How to re-throw any error with `?`

In Java, there's a JIT that optimizes this at runtime. In Rust, there's no JIT, so Box<dyn> remains a vtable. This is slow because vtables cause cache misses, as the CPU can't predict them accurately

For example

use std::time::Instant;

trait Shape {
    fn area(&self) -> f64;
}

struct Circle {
    radius: f64,
}

struct Rectangle {
    width: f64,
    height: f64,
}

impl Shape for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * self.radius * self.radius
    }
}

impl Shape for Rectangle {
    fn area(&self) -> f64 {
        self.width * self.height
    }
}

fn sum_area_dynamic(shapes: &[Box<dyn Shape>]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn sum_area_static<T: Shape>(shapes: &[T]) -> f64 {
    shapes.iter().map(|s| s.area()).sum()
}

fn benchmark_dynamic(n: usize, iterations: u32) -> std::time::Duration {
    let shapes: Vec<Box<dyn Shape>> = (0..n)
        .map(|i| -> Box<dyn Shape> {
            if i % 2 == 0 {
                Box::new(Circle { radius: i as f64 + 1.0 })
            } else {
                Box::new(Rectangle { width: i as f64 + 1.0, height: i as f64 + 2.0 })
            }
        })
        .collect();

    let start = Instant::now();
    let mut total = 0.0f64;
    for _ in 0..iterations {
        total += sum_area_dynamic(&shapes);
    }
    let elapsed = start.elapsed();
    std::hint::black_box(total);
    elapsed
}

fn benchmark_static_circle(n: usize, iterations: u32) -> std::time::Duration {
    let shapes: Vec<Circle> = (0..n)
        .map(|i| Circle { radius: i as f64 + 1.0 })
        .collect();

    let start = Instant::now();
    let mut total = 0.0f64;
    for _ in 0..iterations {
        total += sum_area_static(&shapes);
    }
    let elapsed = start.elapsed();
    std::hint::black_box(total);
    elapsed
}

fn benchmark_static_rectangle(n: usize, iterations: u32) -> std::time::Duration {
    let shapes: Vec<Rectangle> = (0..n)
        .map(|i| Rectangle { width: i as f64 + 1.0, height: i as f64 + 2.0 })
        .collect();

    let start = Instant::now();
    let mut total = 0.0f64;
    for _ in 0..iterations {
        total += sum_area_static(&shapes);
    }
    let elapsed = start.elapsed();
    std::hint::black_box(total);
    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!(
        "{:<40} | {:>10.3} ms | {:>8.3} ns/op",
        label,
        elapsed.as_secs_f64() * 1000.0,
        ns_per_op
    );
}

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

    println!("Benchmark: vtable (Box<dyn>) vs non-vtable (generics/monomorphized)");
    println!("Elements: {n}, Iterations: {iterations}");
    println!("{}", "-".repeat(70));
    println!("{:<40} | {:>10} | {:>12}", "Method", "Total Time", "ns/op");
    println!("{}", "-".repeat(70));

    let d = benchmark_dynamic(n, iterations);
    print_result("Dynamic dispatch (Box<dyn Shape>)", d, iterations, n);

    let sc = benchmark_static_circle(n, iterations);
    print_result("Static dispatch (Circle only)", sc, iterations, n);

    let sr = benchmark_static_rectangle(n, iterations);
    print_result("Static dispatch (Rectangle only)", sr, iterations, n);

    println!("{}", "-".repeat(70));

    let dyn_ns = d.as_nanos() as f64;
    let static_avg_ns = (sc.as_nanos() + sr.as_nanos()) as f64 / 2.0;
    let overhead = ((dyn_ns - static_avg_ns) / static_avg_ns) * 100.0;

    println!(
        "\nVtable overhead vs static avg: {:.2}%",
        overhead
    );
}

The result :

Benchmark: vtable (Box<dyn>) vs non vtable (generics/monomorphized)
Elements: 10000, Iterations: 1000
----------------------------------------------------------------------
Method                                   | Total Time |        ns/op
----------------------------------------------------------------------
Dynamic dispatch (Box<dyn Shape>)        |     34.441 ms |    3.444 ns/op
Static dispatch (Circle only)            |      9.292 ms |    0.929 ns/op
Static dispatch (Rectangle only)         |      9.205 ms |    0.921 ns/op
----------------------------------------------------------------------

Vtable overhead vs static avg: 272.39%

For Box<dyn> as error type :

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

#[derive(Debug)]
enum AppError {
    NotFound(String),
    ParseError(String),
    IoError(String),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(msg) => write!(f, "NotFound: {}", msg),
            AppError::ParseError(msg) => write!(f, "ParseError: {}", msg),
            AppError::IoError(msg) => write!(f, "IoError: {}", msg),
        }
    }
}

impl std::error::Error for AppError {}

#[derive(Debug)]
struct NotFoundError(String);
#[derive(Debug)]
struct ParseError(String);
#[derive(Debug)]
struct IoError(String);

impl fmt::Display for NotFoundError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "NotFound: {}", self.0)
    }
}
impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "ParseError: {}", self.0)
    }
}
impl fmt::Display for IoError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "IoError: {}", self.0)
    }
}

impl std::error::Error for NotFoundError {}
impl std::error::Error for ParseError {}
impl std::error::Error for IoError {}

fn process_dynamic(input: i32) -> Result<i32, Box<dyn std::error::Error>> {
    match input % 3 {
        0 => Err(Box::new(NotFoundError(format!("item {input}")))),
        1 => Err(Box::new(ParseError(format!("value {input}")))),
        _ => Ok(input * 2),
    }
}

fn process_static(input: i32) -> Result<i32, AppError> {
    match input % 3 {
        0 => Err(AppError::NotFound(format!("item {input}"))),
        1 => Err(AppError::ParseError(format!("value {input}"))),
        _ => Ok(input * 2),
    }
}

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                     |    562.580 ms |   56.258 ns/op
Enum AppError   — mixed ok/err                     |    363.631 ms |   36.363 ns/op

[Skenario 2] All Errors (100% Err path — heap alloc in each iteration)
--------------------------------------------------------------------------------
Box<dyn Error>  — all errors                       |    782.548 ms |   78.255 ns/op
Enum AppError   — all errors                       |    519.097 ms |   51.910 ns/op

[Skenario 3] All Ok (100% happy path — error type is never created)
--------------------------------------------------------------------------------
Box<dyn Error>  — all ok                           |     35.413 ms |    3.541 ns/op
Enum AppError   — all ok                           |     14.964 ms |    1.496 ns/op

================================================================================
Vtable overhead Box<dyn Error> vs Enum:
  Mixed ok/err  : +54.71%
  All errors    : +50.75%
  All ok        : +136.66%