Functions

Early Returns

An early return exits a function before control reaches the final expression of its body, immediately transferring control (and the return value, if any) back to the caller using the return keyword.

Syntax of return

The return keyword takes an optional expression that must match the function's declared return type:

  • return expression;: Evaluates expression and exits the function with that value.
  • return;: Exits a function whose return type is the unit type () (equivalent to return ();).
fn safe_divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        return None; // Early return on invalid divisor
    }

    Some(numerator / denominator) // Tail expression for the normal path
}

fn print_positive(n: i32) {
    if n <= 0 {
        return; // Early return from a `()`-returning function
    }

    println!("Positive value: {n}");
}

fn main() {
    println!("10 / 2 = {:?}", safe_divide(10.0, 2.0));
    println!("10 / 0 = {:?}", safe_divide(10.0, 0.0));
    print_positive(-5);
    print_positive(42);
}
10 / 2 = Some(5.0)
10 / 0 = None
Positive value: 42

Guard Clauses vs Deep Nesting

The primary idiomatic use of return in Rust is writing guard clauses at the top of a function. A guard clause checks preconditions, edge cases, or error states upfront and returns immediately, keeping the main logic of the function flat and unindented.

Compare deeply nested conditionals with flat guard clauses:

Nested Pyramid (Harder to Read):        Guard Clauses (Idiomatic):
fn process(input) {                     fn process(input) {
    if cond1 {                              if !cond1 { return Err(...); }
        if cond2 {                          if !cond2 { return Err(...); }
            if cond3 {                      if !cond3 { return Err(...); }
                // main work
            }                               // main work at top indentation
        }                               }
    }
}
fn validate_username(username: &str) -> Result<&str, &'static str> {
    if username.is_empty() {
        return Err("username cannot be empty");
    }

    if username.len() < 3 {
        return Err("username must be at least 3 characters");
    }

    if !username.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return Err("username may only contain ASCII letters, digits, and '_'");
    }

    Ok(username)
}

fn main() {
    for candidate in ["", "ab", "ferris!", "ferris_2024"] {
        println!("{candidate:?} -> {:?}", validate_username(candidate));
    }
}
"" -> Err("username cannot be empty")
"ab" -> Err("username must be at least 3 characters")
"ferris!" -> Err("username may only contain ASCII letters, digits, and '_'")
"ferris_2024" -> Ok("ferris_2024")

Exiting Nested Loops with return

While break only exits the innermost enclosing loop, return exits the entire function immediately, no matter how many nested loops or blocks surround it:

fn contains_pair_with_sum(grid: &[[i32; 3]], target: i32) -> Option<(usize, usize, i32)> {
    for (row_idx, row) in grid.iter().enumerate() {
        for (col_idx, &value) in row.iter().enumerate() {
            if value == target {
                // Exits both `for` loops AND the function in one step
                return Some((row_idx, col_idx, value));
            }
        }
    }

    None
}

fn main() {
    let matrix = [[1, 4, 7], [2, 9, 5], [8, 3, 6]];
    println!("Found 9 at: {:?}", contains_pair_with_sum(&matrix, 9));
    println!("Found 0 at: {:?}", contains_pair_with_sum(&matrix, 0));
}
Found 9 at: Some((1, 1, 9))
Found 0 at: None

return as a Diverging Expression (!)

In Rust's type system, return expr is itself an expression whose type is the never type (!), because evaluation never continues past return into the surrounding expression.

Because ! coerces into any type, return can be placed directly on the right-hand side of a match arm, an if/else branch, or a let ... else statement when assigning to a variable:

fn parse_and_double(input: Option<&str>) -> Result<i32, &'static str> {
    // 1. `return` inside a `match` arm assigned to `text`:
    let text: &str = match input {
        Some(s) if !s.is_empty() => s,
        _ => return Err("missing or empty input"),
    };

    // 2. `return` inside a `let ... else` block:
    let Ok(number) = text.parse::<i32>() else {
        return Err("failed to parse integer");
    };

    Ok(number * 2)
}

fn main() {
    println!("Some(\"21\") -> {:?}", parse_and_double(Some("21")));
    println!("None       -> {:?}", parse_and_double(None));
    println!("Some(\"abc\")-> {:?}", parse_and_double(Some("abc")));
}
Some("21") -> Ok(42)
None       -> Err("missing or empty input")
Some("abc")-> Err("failed to parse integer")

In let text: &str = match input { ... }, the first arm has type &str, while the second arm (return Err(...)) has type !, which coerces to &str so the entire match expression type-checks cleanly.

Resource Cleanup (Drop Order) on Early Return

Returning early never leaks local resources or skips destructors. When a return executes, Rust automatically drops all local variables that have been initialized up to that point in reverse declaration order (LIFO), while skipping destructors for variables declared later in the function that were never initialized:

struct Resource(&'static str);

impl Drop for Resource {
    fn drop(&mut self) {
        println!("  [drop] Cleaning up {}", self.0);
    }
}

fn run_task(abort_early: bool) -> &'static str {
    let _first = Resource("1. database connection");
    let _second = Resource("2. temporary buffer");

    if abort_early {
        println!("Triggering early return...");
        return "aborted";
    }

    let _third = Resource("3. output file");
    println!("Completing full task...");
    "completed"
}

fn main() {
    println!("Result: {}\n", run_task(true));
    println!("Result: {}", run_task(false));
}
Triggering early return...
  [drop] Cleaning up 2. temporary buffer
  [drop] Cleaning up 1. database connection
Result: aborted

Completing full task...
  [drop] Cleaning up 3. output file
  [drop] Cleaning up 2. temporary buffer
  [drop] Cleaning up 1. database connection
Result: completed

Common Errors

Bare return; in a Function That Returns a Value

Using return; without a value in a function whose return type is not () causes a type mismatch (E0069 / E0308):

// ❌ does not compile
fn compute(x: i32) -> i32 {
    if x < 0 {
        return;
    }
    x * 2
}

fn main() {
    println!("{}", compute(5));
}
error[E0069]: `return;` in a function whose return type is not `()`
 --> src/main.rs:3:9
  |
1 | fn compute(x: i32) -> i32 {
  |                       --- return type is not `()`
2 |     if x < 0 {
3 |         return;
  |         ^^^^^^ return omits value
  |
help: provide a value to return
  |
3 |         return /* value */;
  |                +++++++++++

error: aborting due to 1 previous error

Unreachable Code After an Unconditional return

Any statements written after an unconditional return in the same block can never execute and trigger the unreachable_code lint:

fn get_constant() -> i32 {
    return 10;
    let x = 20;
    x
}

fn main() {
    println!("{}", get_constant());
}
warning: unreachable statement
 --> src/main.rs:3:5
  |
2 |     return 10;
  |     --------- any code following this expression is unreachable
3 |     let x = 20;
  |     ^^^^^^^^^^^ unreachable statement
  |
  = note: `#[warn(unreachable_code)]` on by default