Question

Returning Values vs References from Functions in Rust

rustreferencelifetime

Question

I want to write a Rust program that writes to a file in two steps. The file may not exist before the program runs, and the filename is fixed.

The issue is that OpenOptions::new().write(true).open(...) can fail. When that happens, I want to call a custom function such as trycreate() to create the file and return a usable file handle.

I tried returning a reference from the function, but that does not work because the value is created inside the function and I cannot give the returned reference a valid lifetime.

How should this be handled correctly in Rust?

Here is the code I started with:

use std::fs::OpenOptions;
use std::io::Write;

fn trycreate() -> &OpenOptions {
    let f = OpenOptions::new().write(true).open("foo.txt");
    let mut f = match f {
        Ok(file) => file,
        Err(_) => panic!("ERR"),
    };
    f
}

fn main() {
    {
        let f = OpenOptions::new().write(true).open("foo.txt");
        let mut f = match f {
            Ok(file) => file,
            Err(_) => trycreate(),
        };

        let buf = b"test1\n";
        let _ret = f.write(buf).unwrap();
    }

    println!("50%");

    {
        let f = OpenOptions::new().append(true).open("foo.txt");
        let mut f = match f {
            Ok(file) => file,
            Err(_) => panic!("append"),
        };

        let buf = b"test2\n";
        let _ret = f.write(buf).unwrap();
    }

    println!("Ok");
}

Short Answer

By the end of this page, you will understand why Rust does not allow returning a reference to a local variable, when to return an owned value instead, and how to properly open or create files using std::fs::File and OpenOptions. You will also see practical Rust patterns for error handling in file operations.

Concept

In Rust, a reference must always point to data that is still alive. A value created inside a function is dropped when that function ends, so returning a reference to it would create a dangling reference. Rust prevents this at compile time.

That is the core reason your function cannot return something like &OpenOptions or &File when the value is created locally inside the function.

Why this matters

Rust’s ownership system is designed to guarantee memory safety without a garbage collector. One of its key rules is:

  • You may return owned data freely
  • You may only return references if the referenced data outlives the function call

In your example, the file handle is created inside the function. That means the function should return the file handle by value, not by reference.

Important detail: OpenOptions is not the file handle

There are two different types involved:

  • OpenOptions: a builder used to configure how a file should be opened
  • File: the actual file handle you read from or write to

So if your goal is to create or open a file and then write to it, the function should usually return:

std::fs::File

or more commonly:

std::io::Result<std::fs::File>

That lets the caller handle success or failure cleanly.

Better approach

Instead of:

  • opening a file
  • failing
  • calling another function that tries to create it
  • returning a reference

Rust usually encourages one of these patterns:

  1. Use OpenOptions with .create(true) so opening and creating are handled in one step
  2. Return Result<File> from helper functions
  3. Return owned values, not references, for locally created resources

Mental Model

Think of a function like a workshop.

  • If the workshop builds a tool and gives you the tool, that is returning an owned value.
  • If the workshop tries to give you a borrowed pointer to a tool still sitting inside the workshop, that is unsafe once the workshop closes.

When the function ends, its local variables are cleaned up. So a reference to a local variable would be like giving someone directions to a table that gets removed immediately after they leave.

In Rust:

  • returning a value = handing over the actual tool
  • returning a reference = lending access to something that must remain alive elsewhere

If the function creates the file handle, it should hand over the handle itself.

Take Quiz

Syntax and Examples

The usual Rust solution is to return an owned File inside a Result.

Basic syntax

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

fn create_file() -> io::Result<File> {
    File::create("foo.txt")
}

This works because File is returned by value.

Example: open for writing, create if missing

use std::fs::OpenOptions;
use std::io;

fn open_or_create() -> io::Result<std::fs::File> {
    OpenOptions::new()
        .write(true)
        .create(true)
        .open("foo.txt")
}

What this does

  • .write(true) enables writing
  • .create(true) creates the file if it does not exist
  • .open("foo.txt") returns Result<File>

This is often all you need.

Example: use the returned file handle

use std::fs::OpenOptions;
use std::io::{self, Write};

fn open_or_create() -> io::Result<std::fs::File> {
    OpenOptions::new()
        .write(true)
        .create(true)
        .open("foo.txt")
}

fn main() -> io::Result<()> {
    let mut f = open_or_create()?;
    f.write_all(b"test1\n")?;
    Ok(())
}

Why this is better

  • no invalid references
  • no manual match needed for simple propagation
  • errors are handled using Result
  • the file handle is returned safely as an owned value

Step by Step Execution

Consider this example:

use std::fs::OpenOptions;
use std::io::{self, Write};

fn open_or_create() -> io::Result<std::fs::File> {
    OpenOptions::new()
        .write(true)
        .create(true)
        .open("foo.txt")
}

fn main() -> io::Result<()> {
    let mut file = open_or_create()?;
    file.write_all(b"hello\n")?;
    Ok(())
}

Step by step

1. main() calls open_or_create()

Rust enters the helper function.

2. OpenOptions::new() creates a builder

This builder stores file-opening options.

3. .write(true) sets write mode

The file will be opened for writing.

4. .create(true) says “create if missing”

If foo.txt does not exist, Rust asks the operating system to create it.

5. .open("foo.txt") runs the operation

This returns either:

  • Ok(file) if successful
  • Err(error) if it fails

6. open_or_create() returns that Result<File>

The File is moved out of the function, so there is no lifetime problem.

7. let mut file = open_or_create()?;

The ? operator means:

  • if successful, extract the File
  • if it failed, return the error from main

8. file.write_all(b"hello\n")?;

The file handle is used to write bytes.

9. main() returns Ok(())

The program ends normally, and the file is closed automatically when file goes out of scope.

Real World Use Cases

Returning owned resources instead of references is common in many Rust programs.

File handling

  • Open a log file and return File
  • Create a report file if it does not exist
  • Append to a data export file

Network code

  • Return a TcpStream created inside a helper function
  • Return a configured HTTP client object

Data processing

  • Return a parsed String, Vec<T>, or struct from a function
  • Return a loaded configuration object from disk

Database or service setup

  • Return a connection object created by a setup function
  • Return a configured application state struct

In all of these cases, the function creates something and gives ownership of it to the caller.

Take Quiz

Real Codebase Usage

In real Rust projects, developers usually combine owned returns with Result-based error handling.

Common patterns

1. Helper functions return Result<T>

fn open_log_file() -> std::io::Result<std::fs::File> {
    std::fs::OpenOptions::new()
        .append(true)
        .create(true)
        .open("app.log")
}

This keeps setup logic reusable.

2. Use ? for clean error propagation

fn save_message(msg: &[u8]) -> std::io::Result<()> {
    let mut file = open_log_file()?;
    file.write_all(msg)?;
    Ok(())
}

3. Use create(true) instead of separate open/create logic

This avoids unnecessary branching and simplifies code.

4. Use append mode for multi-step writes

If your program writes in stages, developers often do this:

let mut file = OpenOptions::new()
    .append(true)
    .create(true)
    .open("foo.txt")?;

That works whether the file already exists or not.

5. Guard clauses for invalid states

If a path or configuration is wrong, return early with an error rather than building complex nested matches.

Practical rewrite of your use case

use std::fs::OpenOptions;
use std::io::{self, Write};

fn main() -> io::Result<()> {
    {
        let mut f = OpenOptions::new()
            .write(true)
            .create(true)
            .open("foo.txt")?;
        f.write_all(b"test1\n")?;
    }

    println!("50%");

    {
        let mut f = OpenOptions::new()
            .append(true)
            .create(true)
            .open("foo.txt")?;
        f.write_all(b"test2\n")?;
    }

    println!("Ok");
    Ok(())
}

This is the idiomatic Rust approach.

Common Mistakes

1. Returning a reference to a local variable

This is the main issue in the original code.

Broken code

fn make_file() -> &std::fs::File {
    let f = std::fs::File::create("foo.txt").unwrap();
    &f
}

Why it fails

f is dropped when the function ends, so &f would point to invalid memory.

Fix

Return the file by value:

fn make_file() -> std::io::Result<std::fs::File> {
    std::fs::File::create("foo.txt")
}

2. Returning the wrong type

OpenOptions is not a file handle.

Broken idea

fn trycreate() -> &OpenOptions

Why it is wrong

You want something writable. That is a File, not OpenOptions.

Fix

Return File or Result<File>.

3. Reimplementing create(true) manually

Beginners often try to:

  • open the file
  • if it fails, create it separately

But Rust already supports this directly.

Better code

OpenOptions::new().write(true).create(true).open("foo.txt")

4. Using write instead of write_all

write may write only part of the buffer.

Less safe

f.write(buf)?;

Better

f.write_all(buf)?;

5. Mixing byte strings and normal strings incorrectly

In your original code, one call used b"foo.txt".

Problem

Most file-opening APIs expect a path-like string such as "foo.txt", not a byte string literal.

Better

.open("foo.txt")

6. Panicking for normal file errors

panic! is usually too aggressive for expected I/O failures.

Better pattern

Return Result and let the caller decide what to do.

Comparisons

ConceptWhat it meansGood forNot good for
Return File by valueTransfer ownership of the file handleLocally created resourcesShared borrowing from elsewhere
Return &FileBorrow an existing file handleWhen the file is owned outside the functionValues created inside the function
Return Result<File>Return a file handle or an errorReal file I/O codeCases where failure is impossible
Use OpenOptions::create(true)Open the file, creating it if neededSimple open-or-create logicCustom branching you do not need
Use File::create()Create or truncate a fileFresh output filesAppending to existing data
Use OpenOptions::append(true)Write to the end of a fileLogs and multi-step writesOverwriting from the start

write vs write_all

MethodBehavior
writeMay write only part of the buffer
write_allKeeps writing until the full buffer is written or fails

File::create vs OpenOptions

APIBest use
File::create("foo.txt")Simple create/truncate case
OpenOptions::new()When you need write, append, create, truncate, or combinations
Take Quiz

Cheat Sheet

Core rule

  • Do not return a reference to a local variable.
  • Return the value itself if the function creates it.

File-related types

use std::fs::{File, OpenOptions};
use std::io;
  • OpenOptions = configuration builder
  • File = actual open file handle

Correct return type for a helper

fn open_file() -> io::Result<File>

Open or create a file

OpenOptions::new()
    .write(true)
    .create(true)
    .open("foo.txt")

Append or create a file

OpenOptions::new()
    .append(true)
    .create(true)
    .open("foo.txt")

Write safely

file.write_all(b"hello\n")?;

Prefer ? over large match blocks

let mut file = open_file()?;

Common mistake

fn bad() -> &File

This is wrong if the File is created inside bad().

Good pattern

fn good() -> io::Result<File> {
    File::create("foo.txt")
}

FAQ

Why can’t I return a reference to a local variable in Rust?

Because local variables are dropped when the function ends. A reference to them would become invalid, and Rust prevents that.

Should I return OpenOptions or File?

Return File for actual file access. OpenOptions is only a builder used to configure how the file is opened.

What is the idiomatic Rust way to open a file if it may not exist?

Use OpenOptions::new().write(true).create(true).open("file").

When should I use append(true)?

Use it when you want each write to go to the end of the file instead of overwriting existing contents.

Why is write_all usually better than write?

Because write may write only part of the buffer, while write_all tries until everything is written or an error occurs.

Is panic! appropriate for file open failures?

Usually no. File errors are common runtime conditions, so returning Result is typically better.

Can a function ever return a reference in Rust?

Yes, but only if the referenced value already exists outside the function and its lifetime is tied correctly to the return type.

Take Quiz

Mini Project

Description

Build a small Rust program that writes a progress log to a file in multiple steps. This demonstrates the correct way to open or create a file, append later, and return owned values instead of invalid references.

Goal

Create a program that writes two lines to the same file safely, even if the file does not exist yet.

Requirements

[ "Create or open a file named progress.txt for the first write.", "Write a first line such as step 1 complete.", "Open the same file again in append mode.", "Write a second line such as step 2 complete.", "Use Result-based error handling instead of panic! for normal I/O flow." ]

Take Quiz

Keep learning

Related questions

View more