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%