14.3 Using ? in main() with Box<dyn Error>
Alright, let’s talk about one of the first real-world problems you’ll hit when you start writing proper Rust applications: main() is a special snowflake. It’s the entry point, and by default, it returns (). That’s great for “hello world,” but the moment you start doing anything that can fail—reading a file, parsing arguments, connecting to a database—you’re stuck. Your functions return Result, but main can’t. So you end up with a forest of .expect() calls, turning your elegant error-handling into a messy series of potential runtime aborts.
We’re better than that. Let’s fix it.
The Problem: main() Can’t Be Bothered
Here’s the classic, frustrating scenario. You try to use ? in main and the compiler, in its infinite wisdom, slaps you down.
use std::fs;
fn main() {
let data = fs::read_to_string("very_important_config.json")?;
println!("{}", data);
}
The error is a classic: “the ? operator can only be used in a function that returns Result or Option”. main returns (), not Result. You’re forced to handle errors manually right there, which often leads to the lazy .expect("file not found") or matching on the Result and printing something before exiting. It works, but it’s inelegant and doesn’t compose well.
The Solution: Making main() Work for a Living
Turns out, main is more flexible than we thought. Since Rust 1.26, we can declare main to return a Result<(), E>. When main returns Ok(()), the program exits with status code 0. If it returns an Err, it prints the error’s Debug representation and exits with a non-zero status code. This is exactly what we want.
But what is E? What type of error can we return? This is where the magic—and the first questionable choice you’ll encounter—comes in.
Enter Box
The problem is that your program could fail in a dozen different ways: std::io::Error, std::num::ParseIntError, serde_json::Error, your custom error type… There’s no single concrete type that covers all of them. We need a trait object.
Box<dyn std::error::Error> is the standard way to say “a boxed-up thing that implements the Error trait.” It’s a catch-all. Any type that implements Error can be automatically converted into this boxed trait object using ?. This is the secret sauce.
use std::fs;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let data = fs::read_to_string("very_important_config.json")?;
println!("{}", data);
Ok(())
}
Boom. Now it works. The ? operator desugars like this: if read_to_string returns an Err(std::io::Error), the ? will From::from that error, converting it into a Box<dyn Error>, and then return it early from main. The runtime takes care of the rest.
The Rough Edge: The Dreaded Exit Code 1
Here’s the first pitfall. The error is printed, but it’s printed using its .debug() representation. For many errors, this is fine. For some, it’s a bit verbose and ugly. More importantly, all errors, regardless of their type, cause the program to exit with status code 1.
This is the part that feels like a questionable design choice. Sometimes you want different error types to yield different exit codes. A “file not found” error might be a 1, but a “permission denied” might be a 2, etc. With Box<dyn Error>, you lose that granularity.
If you need custom exit codes, you have to handle the error yourself at the top level, before returning from main. You can do this by matching on a concrete error type or by downcasting the boxed error.
use std::process;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let config = "very_important_config.json";
let data = fs::read_to_string(config)
.map_err(|e| {
eprintln!("Could not read file '{}': {}", config, e);
process::exit(2); // Custom exit code
})?;
println!("{}", data);
Ok(())
}
This approach gives you maximum control but sacrifices the conciseness of ?.
Best Practice: It’s a Trap (For Binaries)
This pattern is perfect for binaries—the main application you’re building. It’s the right way to bubble up errors cleanly. However, do not do this in a library. A library’s public API should return well-defined, concrete Result types with your own specific error enum. Returning Box<dyn Error> from a library function is a cop-out; it forces the user to downcast to figure out what went wrong, which is a horrible experience. Save the trait objects for the application boundary.
So, in summary: use -> Result<(), Box<dyn Error>> for your main function to leverage the ? operator everywhere. It’s clean, idiomatic, and gets you 90% of the way to professional error handling. Just be aware that you’re trading away fine-grained control over exit codes for that simplicity. It’s a trade-off worth making for most utilities, but for core application logic, you might need to roll up your sleeves and handle the errors at the source.