What is the default return value of the "return" expression?

There's the code:

fn main() {
    return; // return -> () ???
}
  1. What does return return without explicitly specifying a value?
  2. Is it true that expressions return a unit type or never type (break, continue) by default?

If "return;" is a statement, how does it return a value? Isn't the return done by the implicit last expression which is an empty tuple?

fn main() -> () {
    return; // don't return value
    () // implicit return value
}

or

fn main() -> () {
    return; // same as `return ();`
}

Sorry, return, break and continue evaluate to the ! (never) type:

break, continue and return expressions also have type !.

! can be coerced to any other type, including the unit type, which is why my previous answer worked, it simply coerced ! to ().


return is an expression, return; is a statement.

1 Like

fixed the question

return is the same as return (), and function return type is the same as -> () written explicitly.

() is a zero-sized "nothing" type and () is also a value of this type.

2 Likes

Okey, but what's really going on here?

fn main() {
    return;
}

return causes the function to exit early, leaving ! as the return value. The ! value is then coerced to a empty tuple. Am I right?

There is a difference between the return value and the value that return evaluates to. The return expression evaluates to !, whereas the return; statement returns () from the function. I.e. this compiles:

fn main() -> () {
    let _: ! = return;
}

but this fails

fn main() -> ! {
    let _: ! = return;
}

with the following error:

error[E0069]: `return;` in a function whose return type is not `()`
 --> src/main.rs:4:16
  |
4 |     let _: ! = return;
  |                ^^^^^^ return type is not `()`

For more information about this error, try `rustc --explain E0069`.
3 Likes

The return expression causes execution of the function to end early. It never gets to the implicit last expression.

3 Likes

! is a magic type that can be substituted for (almost) any other type.

let x: String = panic!();

! is the never type. You can never create/instantiate a value with this type and that is it's purpose.

It's how the type system reasons about code like:

fn example(value: u32) {
    let percentage: u32 = if value > 100 {
        panic!("value too big")
    } else {
        value
    };
    println!("{percentage}")
}

panic!() is the never type (same as return) which is how the compiler allows that block to evaluate to a u32 type, even though there's a branch that doesn't return a u32. If that branch is followed, it'll never return anything because it instead panics.

This is a distinct concept from (), the unit type. The unit type is possible to return, but there's no data there at all.

The following won't compile:

fn bad_example(value: u32) {
    let percentage: u32 = if value > 100 {
        println!("value too big")
    } else {
        value
    };
    println!("{percentage}");
}

The () returned from println!() is assigned to percentage, whereas the ! never type is never returned and never gets assigned.

Examples in the playground

Admittedly these examples are a bit contrived :slight_smile:

EDIT: when I say panic!() "is" the never type, I am deliberately avoiding saying panic!() "returns" the never type (how could it? you can never make a never type to return) but "is" might be confusing too. Don't read too much into my choice of the word "is" there.

1 Like
fn main() {
    return;
}

is indeed sugar for

fn main() -> () {
    return ();
}

and it works the same way as something like

fn not_main() -> bool {
    return false;
}

works.

But how does it work?

The return keyword is used for early returns in Rust. Early returning is one of multiple primitives in Rust that can break ordinary control-flow. Other such examples are panicking; and things like break/continue.

return () is an expression of type !. The type of an expression is the type of the value the expression will evaluate to. So "return () is an expression that evaluates to a value of type !" Sometimes colloquially, this is also called the value the expression "returns", however as you may notice, that's confusing, because there's also the early-return from a function via return … expressions, so let's avoid that confusion and speak of "evaluating to" instead.

The type ! has no values. So the whole statement of "return () is an expression that evaluates to a value of type !" is a sort of fiction. It can never actually evaluate to a value of type !, because there are no values of type !.

Why this complicated fiction? It's actually simpler this way. By just assuming that "every expression can evaluate to a value", we keep things uniform. Expressions that don't evaluate to a value are just ordinary expressions, still, and it's only the type that ultimately gives the information that there won't be any value, after all.

About your question of 'coercion', ! can coerce to () indeed, something like this is accepted by the compiler:

fn not_main() -> bool {
    let coerced_val: () = return false;
}

but a value of type ! will never be coerced to a value of type (), because ! has no value.

In the example of

fn not_main() -> bool {
    return false;
}

or

fn main() -> () {
    return ();
}

arguably no coercion ever needs to happen. A statement like return false; (mind the semicolon which makes this into a statement) just evaluates the expression return false and ignores its result whatever type it is, the type of the expression needs not match the return type of the function. Now, if you instead wrote

fn not_main() -> bool {
    return false
}

without a semicolon, then the type checker would indeed check "can ! coerce to bool?" because now return false is itself in the position where the whole body of the function evaluates to this expression (answer: yes, the type ! can coerce to bool [or any type, really]; an actual coercion will never happen at run-time because !-values do not exist)

Speaking of types without a value: There is also a sense in which () has no "value", the "unit" type is quite "empty" itself, but that's a different kind of "empty". Indeed () has a value, it's just a single value, the value () (perhaps confusingly using the same syntax as its type). The information here however is none. A single value is only a single choice, you need zero bits to encode this. So in a very different sense of "does not exist", a value of "()" also "does not exist" at run-time. Yes, using natural language (English) to talk about technically precise stuff can be confusing.

If you happen to undestand Rust enums, then ! is like

enum Never {}

and () is like

enum Unit {
    Unit
}

and the () expression would then be Unit::Unit.


So, given an expression of type ! can never evaluate to any value. What can happen during evaluation, instead then!? There are two possibilities:

  • (1) evaluation never ends; this is why e.g. a loop {} expression without any break will have type !.
  • (2) evaluation does end, but does so by breaking ordinary control flow. Normal control flow would evaluate the expression, which can have side-effects, and then ends up with a value which is then passed to whatever context the expression was used in. The expression bar(x) in foo(bar(x)) is passed on to foo, in let var = bar(x); it's assigned to var, etc. Breaking control flow means that all this normal context is skipped and never executed. The program instead goes somewhere else; which can be, for example
    • back where the surrounding function was called, if early-returning via return … expressions
    • where the surrounding loop (loop { } or while … { } or for … in … { }) is evaluated, when break or break … is used
    • where the surrounding catch_unwind was used, when panicking

Also note: Sort-of "on the way" to that location for the program to continue at, often also some destructors are executed first.

An expression of any so-called "inhabited" type correspondingly has 3 possibilities: (1) evaluation completes and produces a value, (2) evaluation never ends, (3) evaluation ends breaking ordinary control flow.

If you count each different value of the type as their own possibilities, then it's more. A bool expression can evaluate to true or to false or never terminate or panic or break control flow. Yes, some people probably like to differentiate the last two cases, too. The more possible values, the more possibilities; with bool having 2 values, () having 1 value, ! having 0 values, it's unsurprising that evaluation to type ! has fewer possible outcomes.

Also note that across function boundaries, panicking is the only way of "breaking control flow". You can't write break in a function and expect a loop outside the function to be exited. So if you speak about "what can happen executing a function of return type !", only the possibilities of "never stops executing" and "panics" are left.


Another complication: If you look back at something like

fn not_main() -> bool {
    return false;
}

the return false is an early-return of type bool, but where is the non-early return? If the compiler treats ! like any other type, why doesn't it complain unless we gave it also a return value for the normal control flow, e.g. like the following?

fn not_main() -> bool {
    return false;
    true
}

Answer: There is special handling of ! in the compiler after all. Normally, a function body (or any other block) without a final result-expression that it can evaluate to, can only evaluate to () implicitly.

E.g.

let x = {
    let y = foo;
    bar(y);
};

is like sugar for

let x = {
    let y = foo();
    bar(y);
    ()
};

and x gets assigned ().

But if the compiler can determine that a function body (or any other block) will have to evaluate an expression of type !, and there's no way around that, then the whole block can have type ! (and thus coerce to any type) instead. So e.g. this works, too

let x: String = {
    let y = foo();
    bar(y);
    if foo() {
        return;
    } else {
        panic!();
    }
    foo();
};

even though there's never any String produced anywhere; the type-checker can still accept it.
It certainly is not desugaring to

let x: String = {
    let y = foo();
    bar(y);
    if foo() {
        return;
    } else {
        panic!();
    }
    foo();
    () // <- new line added
};

as that wouldn't compile.

Instead it's a bit like if it desugared to something like this instead:

let x: String = {
    let y = foo();
    bar(y);
    if foo() {
        return;
    } else {
        panic!();
    }
    foo();
    unreachable!()
};

where unreachable!() is giving an explicit !-typed result expression for the block. But the compiler only does this if this place was really for sure known to already be unreachable anyways; whereas the unreachable!() macro works simply by panicking. [Though common practice considers it always a bug in your program, if that panicking can ever happen at run-time in your program.]

Same applies to

fn not_main() -> bool {
    return false;
}

which you can - if you find this useful - soft-of think of to be 'desugaring' to something like

fn not_main() -> bool {
    return false;
    unreachable!()
}

which also means there kind-of is something akin to a "coercion from ! to bool" here, after all.

Coming back to the original

fn main() {
    return;
}

the same applies, here, too, but for () instead of bool.

6 Likes

I'll note that the Rust reference is wrong about this:

The type of a block is the type of the final operand, or () if the final operand is omitted.

It's missing the case where the end of the block is unreachable.

4 Likes

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.