The scenario
You're refactoring some file I/O code, you slap a ? on a File::open() call to skip the verbose .unwrap(), and Rust immediately fires back:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:3:29
|
3 | let f = File::open("data.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
Your function returns () โ nothing. The ? operator needs somewhere to propagate the error. No Result in scope means nowhere for it to go.
Why this happens
The ? operator is syntactic sugar for an early return on error. Writing some_fn()? desugars to roughly:
match some_fn() {
Ok(val) => val,
Err(e) => return Err(e.into()), // early return
}
That return Err(e) requires the enclosing function to return a Result. If it doesn't, there's no valid return path. The same logic applies to Option: ? on an Option returns None early, so the function must return Option too.
Fix 1: Change main() to return Result
main() is where this error bites most often. Add a return type and a final Ok(()):
use std::fs::File;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut f = File::open("data.txt")?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("{}", contents);
Ok(())
}
Box<dyn std::error::Error> is the catch-all โ it accepts any error that implements the Error trait. Good enough for most binaries and one-off scripts.
If you use the anyhow crate, it's even cleaner:
use anyhow::Result;
fn main() -> Result<()> {
let contents = std::fs::read_to_string("data.txt")?;
println!("{}", contents);
Ok(())
}
Fix 2: Change a regular function's return type
Any function can use ? โ it just needs the right return type. Here's the before and after:
// Before โ compiler error
fn load_config() {
let contents = std::fs::read_to_string("config.toml")?;
// ...
}
// After โ compiles
fn load_config() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("config.toml")?;
Ok(contents)
}
Dealing with multiple error types in one function? A boxed error saves you from writing manual conversions between them:
fn load_and_parse() -> Result<MyConfig, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("config.toml")?;
let config: MyConfig = toml::from_str(&contents)?; // different error type, no problem
Ok(config)
}
Fix 3: Using ? inside closures
Closures are trickier. This won't compile:
let results: Vec<_> = paths.iter().map(|p| {
let content = std::fs::read_to_string(p)?; // error here
content
}).collect();
The closure's inferred return type is String, not Result<String, _>. Make the closure return Result explicitly, then collect into a Result<Vec> โ this fails fast on the first error:
let results: Result<Vec<_>, _> = paths.iter().map(|p| {
std::fs::read_to_string(p)
}).collect();
match results {
Ok(contents) => { /* all files read */ }
Err(e) => eprintln!("Failed: {}", e),
}
Need to skip failures silently instead? Use .ok() with filter_map:
let contents: Vec<String> = paths.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect();
Fix 4: Mixing Result and Option
Mixing Result and Option in one function trips people up. They're different types โ you can't use ? on both without conversion:
use std::collections::HashMap;
// Error โ function returns Result but ? is used on an Option
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key)?; // Option, not Result!
Ok(val.clone())
}
Convert Option to Result with .ok_or():
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key).ok_or_else(|| format!("key '{}' not found", key))?;
Ok(val.clone())
}
Flip direction โ function returns Option, but you have a Result โ use .ok() to discard the error:
fn try_read(path: &str) -> Option<String> {
std::fs::read_to_string(path).ok()
}
Verify the fix
Before doing a full build, run a fast type check:
cargo check
No E0277 means you're good. Then run your tests:
cargo test
Changed main()'s signature? Do a full build to confirm nothing breaks at runtime:
cargo run
Quick reference
- In main() โ change to
fn main() -> Result<(), Box<dyn Error>>, addOk(())at the end - In a function โ change return type to
Result<T, E>orOption<T>, return the value wrapped inOk()orSome() - In a closure โ make the closure return
Result, collect intoResult<Vec<_>, _> - Mixing types โ use
.ok_or()to convertOption โ Result, or.ok()to convertResult โ Option
E0277 is one of Rust's more helpful errors โ it tells you exactly which function needs a different return type and points directly at the offending ?. Once the return types line up, ? is one of the cleanest error propagation patterns in any systems language.

