1

I am pretty confused on the ? operator in functions that return Result<T, E>.

I have the following snippet of code:

use std::error;
use std::fs;


fn foo(s: &str) -> Result<&str, Box<error::Error>> {
    let result = fs::read_to_string(s)?;
    return Ok(&result);
}

fn bar(s: &str) -> Result<&str, &dyn error::Error> {
    // the trait `std::convert::From<std::io::Error>` is not implemented for `&dyn std::error::Error` (1)
    let result = fs::read_to_string(s)?;
    return Ok(&result);
}

fn main() {
    println!("{}", foo("foo.txt").unwrap());
    println!("{}", bar("bar.txt").unwrap());
}

As you might see from the above snippet, the ? operator works pretty well with the returned boxed error, but not with dynamic error references (error at (1)).

Is there any specific reason why it does not work? In my limited knowledge of Rust, it is more natural to return an error reference, rather than a boxed object: in the end, after returning rom the foo function, I expect deref coercion to work with it, so why not returning the error reference itself?

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
fcracker79
  • 1,118
  • 13
  • 26

2 Answers2

3

Look at this function signature:

fn bar(s: &str) -> Result<&str, &dyn error::Error> {

The error type is a reference, but a reference to what? Who owns the value being referenced? The value cannot be owned by the function itself because it would go out of scope and Rust, quite rightly, won't allow you to return the dangling reference. So the only alternative is that the error is the input string slice s, or some sub-slice of it. This is definitely not what you wanted.

Now, the error:

the trait `std::convert::From<std::io::Error>` is not implemented for `&dyn std::error::Error`

The trait isn't implemented, and it can't be. To see why, try to implement it by hand:

impl<'a> From<io::Error> for &'a dyn error::Error {
    fn from(e: io::Error) -> &'a dyn error::Error {
        // what can go here?
    }
}

This method is impossible to implement, for exactly the same reason.


Why does it work for Box<dyn Error>? A Box allocates its data on the heap, but also owns that data and deallocates it when the box goes out of scope. This is completely different from references, where the owner is separate, and the reference is prevented from outliving the data by lifetime parameters in the types.

See also:

Peter Hall
  • 53,120
  • 14
  • 139
  • 204
1

Although it is possible to cast the concrete type std::io::Error into dyn Error, it is not possible to return it as a reference because the "owned" value is being dropped/erased/removed at the end of the function, same goes to your String -> &str. The Box<error::Error> example works because an owned Error is created in the heap (Box<std::io::Error>) and the std has an implementation of Error for Box<T> (impl<T: Error> Error for Box<T>).

If you want to erase the concrete type and only work with the available methods of a trait, it is possible to use impl Trait.

use std::{error, fs};

fn foo(s: &str) -> Result<String, Box<dyn error::Error>> {
    let result = fs::read_to_string(s)?;
    Ok(result)
}

fn bar(s: &str) -> Result<String, impl error::Error> {
    let result = match fs::read_to_string(s) {
        Ok(x) => x,
        Err(x) => return Err(x),
    };
    Ok(result)
}

fn main() {
    println!("{}", foo("foo.txt").unwrap());
    println!("{}", bar("bar.txt").unwrap());
}
Caio
  • 3,178
  • 6
  • 37
  • 52