How Err is being returned for incompatibel match arms?

Hi Rustaceans,

I'm starting to use rust and currently learning the language.

I understand that arms in match expression should be of compatible types. Therefore if we go through this code: Ref

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

It raises compilation error as the 1st match expression has File struct and Enum Err which are incompatible. That is good.

If add return to the Enum Err there's no compilation error.

use std::fs::File;
use std::io;
use std::io::Read;

fn read_username_from_file() -> Result<String, io::Error> {
    let f = File::open("hello.txt");

    let mut f = match f {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut s = String::new();

    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

How's this possible ? Isn't we still have incompatible types.

Return statements evaluate to the Never type (!), which can be coerced into any other type.

In case you aren't aware of it, for this pattern one would usually use the question mark operator.

    let mut f = f?;

Or, combined with the previous line:

    let mut f = File::open("hello.txt")?;

Also note the existence of the convenience function fs::read_to_string

Hi @steffahn,

I know about the ?.

But I want to be clear with the normal way of passing down the errors that I've mentioned in my first post.

But all the return statements will give the types defined after it right ? As in

return 1 => i32 type
return "dfg" => &str type
return 3.4 => f64 type

Then we would get Err enum only right ?

I'm not sure what you mean by “all the return statements will give the types defined after it”. In particular the use of “defined” makes no sense in my head, and I'm not sure if you're referring to the function return value or the type/value of the return expression itself by saying “give”.

Err would usually not be called an enum. Instead it's an enum variant, in particular its an abbreviation of the variant Result::Err of the enum “Result”.

Or maybe it's even more correct with the parameters: If you have an expression ex of type E and another type T, then Err(e) is a value of the “Err”-variant of the enum Result<T, E>. The expression Err acts as a constructor for the enum Resukt<T, E>. It itself can be regarded (for construction purposes) as a function fn(E) -> Result<T, E>.

The statement return Err(e) gets the argument Err(e) which is the enum variant constructor Err applied to the value e. Regarding types, the type Err(e) must match the return type of the function, Result<String, io::Error>, which it does, since e is an io::Error and Err(…) can construct any Result<T, io::Error> for abitary types T from this.

Regarding the expression itself, return Err(e) doesn't evaluate to any value, since evaluating it immediately exits the function. In the type system, it has the so-called “never type”, written “!”, which is a type that has no value. Types without value, also called uninhabited types, can only appear in parts of code that are unreachable (unless you're doing things very very wrong with unsafe code). The (small but conceptually existing) part of the code where the return Err(e) expression has been evaluated and its “result” is taken to be the result of the whole match expression is technically unreachable. The never type is a special uninhabited type that can coerce into (i.e. be implicitly converted into) any other type. Maybe this version makes things clearer:

    let mut f = match f {
        Ok(file) => file,
        Err(e) => {
            let value = return Err(e);
            // this part of the code is unreachable and the
            // variable “value” has type “!”.
            value // value of type ! can be coerced into File
        }, 
    };

So (both in this code and in the original one without the intermediate variable value) from the type checkerʼs point of view, the first match am evaluates to a File and the second one to a value of type !, which can be coerced to File, so everything type-checks and he value of the whole match expression is a File. From an intuitive understanding, the return expression just doesnʼt evaluate to anything at all, so it doesnʼt matter if it appears in a match expression or anywhere else, youʼll never have to worry about creating an expression of the right type if youʼve already exited the function. For this reason, something like this works as well:

    let mut f = match f {
        Ok(file) => file,
        Err(e) => {
            return Err(e); // works even with the semicolon
            // so - syntactically - this block doesn't even try to
            // return a value at all
        }, 
    };

For more about this, you can try to look at this post of mine in a recent discussion jn a different thread:

2 Likes

Thanks @steffahn for such an elaborate explanation.

Now I understood, how "never type" "!" is tagged to the return <expression>.

I got confused/mistaken that expression after return gets evaluated at compile time and that particular concrete type gets tagged to that whole return <expression>.

This helped me to clear alot.

I'm not sure what you mean by “all the return statements will give the types defined after it”. In particular the use of “defined” makes no sense in my head, and I'm not sure if you're referring to the function return value or the type/value of the return expression itself by saying “give”.

I was referring to only the types of expression in the whole return <expression>

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.