How to create temporary files

Create temporary files in Rust using std::env::temp_dir() and std::fs::File::create(), then manually delete them when finished.

When you need a scratchpad that disappears

You're building a tool that converts images. The conversion library requires a file path to work, but you don't want to leave a 500-megabyte intermediate file cluttering the user's home directory. Or you're writing a config generator that assembles a JSON file before moving it into place. You need storage that exists for a moment, does its job, and then vanishes without a trace.

That's a temporary file. Rust's standard library gives you the path to the system's temporary directory. It does not manage the file's lifecycle. You create the file, you write to it, you delete it. If your program crashes, the file stays. The operating system might clean it up on reboot, but that behavior varies by platform and is not a guarantee. For anything beyond a quick script, the ecosystem relies on the tempfile crate to handle uniqueness and automatic cleanup.

How temporary files work in Rust

The standard library provides std::env::temp_dir() to locate the temporary directory. This function returns a PathBuf pointing to the OS-specific scratchpad. On Linux, this is usually /tmp. On Windows, it points to the user's temp folder under %TEMP%. On macOS, it's /var/folders with a unique hash.

Calling temp_dir() does not create the directory. It assumes the directory exists. If the directory is missing, file creation will fail. The function also does not generate unique filenames. If you append a fixed name like temp.txt, you get a predictable path. Predictable paths in shared directories are dangerous. Two processes running at the same time will collide, overwriting each other's data.

The standard library approach requires manual lifecycle management. You construct the path, create the file, write data, and call std::fs::remove_file to delete it. If any step panics or returns an error before the deletion call, the file leaks. The file remains on disk until the user or the OS cleans it up.

use std::env;
use std::fs::File;
use std::io::Write;

fn main() {
    // Get the OS-specific temporary directory path.
    let temp_dir = env::temp_dir();
    
    // Construct a path inside the temp directory.
    // WARNING: This name is predictable and not unique.
    let path = temp_dir.join("scratch.txt");
    
    // Create the file, truncating it if it already exists.
    let mut file = File::create(&path).expect("Failed to create temp file");
    
    // Write data to the file.
    file.write_all(b"Temporary data").expect("Failed to write");
    
    // Manually remove the file to prevent clutter.
    // If the program panics before this line, the file leaks.
    std::fs::remove_file(&path).expect("Failed to delete temp file");
}

Don't hardcode filenames in the temp directory. Predictable names are race conditions waiting to happen.

Walking through the lifecycle

When you call env::temp_dir(), Rust queries the environment variables or system APIs to find the temp directory. The result is a PathBuf, an owned path that you can modify. Calling join creates a new PathBuf with your filename appended. The join method handles path separators correctly for the platform, so you don't need to worry about slashes or backslashes.

File::create opens the file for writing. If the file exists, it is truncated to zero length. If the file does not exist, it is created. The function returns a File handle wrapped in a Result. You must handle the error, or the program panics. Once you have the file, you can write bytes to it using write_all or other I/O methods.

After writing, you call remove_file to delete the file. This function takes a path reference. It removes the file from the filesystem. If the file is still open by another process, or if permissions prevent deletion, the function returns an error.

The critical weakness here is the gap between creation and deletion. If write_all panics, execution jumps to the panic handler. The remove_file call never runs. The file remains on disk. In a long-running service, these leaked files can fill the disk. In a security-sensitive context, leaked temp files might expose sensitive data.

The race condition trap

Using a fixed filename in the temp directory introduces a time-of-check-time-of-use (TOCTOU) vulnerability. Even if you check whether the file exists before creating it, another process can create the file in the nanosecond between your check and your creation. Or two processes can run simultaneously and both create the file, with the second one overwriting the first.

This is not a theoretical risk. Attackers can exploit predictable temp file names to hijack processes. They create a symlink at the expected path pointing to a sensitive file like /etc/passwd. When your program opens the temp file for writing, it actually writes to the sensitive file. Or they create the file first and read your data as you write it.

The standard library does not protect against this. You must use a method that generates unique names atomically. The tempfile crate solves this by generating random filenames and creating the file in a single atomic operation. The OS ensures that no two processes get the same name.

Automatic cleanup with tempfile

The tempfile crate is the community standard for temporary files. It provides types that manage the file's lifecycle automatically. When the type goes out of scope, the file is deleted. This works even if the program panics. The cleanup happens in the Drop implementation, which runs during stack unwinding.

NamedTempFile is the most common type. It creates a file with a unique name in the temp directory. You can access the path using the path() method. You can read and write using the file handle. When the NamedTempFile is dropped, the file is deleted.

use std::io::Write;
use tempfile::NamedTempFile;

/// Writes data to a temporary file and prints the path.
/// The file is automatically deleted when the function returns.
fn process_temp_data() -> std::io::Result<()> {
    // Create a temporary file with a unique name.
    // The file is created immediately and will be deleted on drop.
    let mut temp_file = NamedTempFile::new()?;
    
    // Write data to the file.
    temp_file.write_all(b"Secure temporary data")?;
    
    // Access the path if needed.
    println!("Temp file is at: {:?}", temp_file.path());
    
    // The file is deleted automatically when temp_file goes out of scope.
    Ok(())
}

Trust NamedTempFile to clean up. The file vanishes when the struct drops, panic or not.

There is a subtle gotcha with NamedTempFile. The file is tied to the struct, not the path. If you call path() and store the path in a variable, then drop the NamedTempFile, the file is deleted immediately. The path still exists in your variable, but the file on disk is gone. Any attempt to open that path will fail. Keep the NamedTempFile alive as long as you need the file.

If you want to keep the file after you're done, call the persist() method. This moves the file to a new location and returns a Persisted handle. The file is no longer temporary. You must manage its cleanup manually.

use std::fs::File;
use tempfile::NamedTempFile;

/// Creates a temp file and persists it to a final location.
fn create_config() -> std::io::Result<File> {
    let mut temp = NamedTempFile::new()?;
    temp.write_all(b"config=true")?;
    
    // Persist the file to a specific path.
    // The file is renamed and no longer auto-deleted.
    let final_path = std::path::Path::new("config.json");
    let persisted = temp.persist(final_path)?;
    
    Ok(persisted)
}

Unnamed temporary files

Sometimes you don't need a filename. You just need a file handle to read or write data. The tempfile::tempfile() function creates an unnamed temporary file. The file exists on disk, but the name is hidden. You cannot access the file by path. You can only use the file handle.

Unnamed temp files are useful for pipes or when you want to ensure no other process can find the file by name. They are also slightly faster because the system doesn't need to generate a unique name string.

use std::io::Write;
use tempfile::tempfile;

/// Writes to an unnamed temporary file.
fn use_unnamed_temp() -> std::io::Result<()> {
    // Create an unnamed temporary file.
    // No path is available. Only the handle exists.
    let mut file = tempfile()?;
    
    file.write_all(b"Hidden data")?;
    
    // The file is deleted when file is dropped.
    Ok(())
}

Keep the handle alive. If you drop the TempFile, the data disappears instantly.

Pitfalls and compiler errors

Temporary files introduce several pitfalls. The most common is leaking files due to panics. If you use the standard library and forget to handle errors, a panic skips cleanup. The tempfile crate prevents this by using Drop.

Another pitfall is assuming the temp directory exists. std::env::temp_dir() returns a path, but it does not create the directory. If the directory is missing, file creation fails. The tempfile crate also assumes the directory exists. It does not create it.

Race conditions occur when you use predictable names. Always use tempfile for unique names. If you must use the standard library, generate a unique name using a UUID crate or random bytes, and use atomic creation flags if available.

Compiler errors can appear when you mishandle paths. If you try to pass a PathBuf where a &Path is expected, the compiler might reject you. PathBuf implements AsRef<Path>, so most functions accept it directly. However, if you move a PathBuf into a closure and try to use it later, the compiler rejects you with E0382 (use of moved value). Borrow the path instead of moving it.

use std::path::PathBuf;

fn example_move_error() {
    let path = PathBuf::from("/tmp/test");
    
    // This moves path into the closure.
    let _closure = || {
        println!("{:?}", path);
    };
    
    // ERROR: E0382 use of moved value `path`
    // println!("{:?}", path);
}

If you move the path, you can't use it again. Borrow the path with &path to avoid the move.

Convention asides

The community convention is to use the tempfile crate for all temporary file operations. It is lightweight, well-tested, and handles edge cases across platforms. Adding tempfile to your project is standard practice. Run cargo add tempfile to include it.

When using NamedTempFile, the convention is to call Rc::clone style explicit methods when needed, but tempfile methods are clear enough. The persist() method is the standard way to convert a temp file to a permanent file. The keep() method is deprecated; use persist() instead.

File permissions are another convention. tempfile creates files with restricted permissions, usually 0o600 on Unix. This prevents other users from reading or writing the file. This is a security best practice. If you need different permissions, create the file with tempfile and then change permissions using std::fs::set_permissions.

Decision matrix

Use std::env::temp_dir() when you need the directory path for a tool that manages its own lifecycle, or when you are implementing a custom storage backend that requires direct path access.

Use tempfile::NamedTempFile when you need a unique filename and want the file to delete automatically when the variable goes out of scope.

Use tempfile::tempfile() when you don't care about the filename and only need a file handle for reading or writing.

Use std::fs::remove_file when you are managing cleanup manually and have confirmed the file exists and is not in use.

Use NamedTempFile::persist() when you need to keep the temporary file at a specific location after processing is complete.

Where to go next