How to use tempfile crate in Rust temporary files

Use tempfile::tempdir() to create auto-deleting temporary directories for safe intermediate file storage in Rust.

The problem with leaving files behind

You are building a command line tool that needs to write intermediate data before producing a final result. Maybe it is a video converter that extracts frames, or a data pipeline that normalizes CSV rows, or a compiler backend that stores temporary bitcode. You write the files to disk, process them, and return the output. Then the user runs the tool again. And again. After a few weeks, their temporary directory is cluttered with orphaned files. Permissions drift. Disk space shrinks. Manual cleanup scripts become a maintenance burden.

Rust solves this with deterministic cleanup. The language guarantees that when a variable leaves scope, its Drop implementation runs immediately. The tempfile crate wraps that guarantee around operating system file creation. You get a path, you use it, and the moment the variable disappears, the file or directory vanishes with it. No manual deletion calls. No cleanup functions to forget. No orphaned artifacts.

How tempfile keeps your disk clean

The crate provides two primary types. NamedTempFile manages a single file. TempDir manages a directory that can hold multiple files. Both follow the same lifecycle pattern. Creation happens in the system temporary directory. The OS assigns a unique name to avoid collisions. The crate hands you a wrapper type that holds the path and a flag indicating whether cleanup should happen on drop. When the wrapper goes out of scope, the Drop trait calls the operating system to remove the file or directory. If your program panics, returns early, or crashes, the cleanup still runs.

Think of it like a hotel room key. You check in, receive the key, and use the room. When you walk out of the lobby, the front desk automatically cleans and locks the room. You never have to fill out a checkout form or call housekeeping. The system handles it because the key itself carries the responsibility.

The minimal setup

Add the crate to your project first. The standard approach uses Cargo.

[dependencies]
tempfile = "3"

Writing to a temporary file requires three steps. Create the wrapper, write data using standard I/O, and let the variable drop.

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

/// Writes a short message to a temporary file and prints its contents.
fn write_and_read_temp() -> std::io::Result<()> {
    // Creates a unique file in the OS temp directory.
    // The file is deleted automatically when `file` drops.
    let mut file = NamedTempFile::new()?;
    
    // Writes bytes to the underlying file descriptor.
    file.write_all(b"temporary data\n")?;
    
    // Flushes the buffer to ensure the OS sees the data.
    file.flush()?;
    
    // Reads the path for demonstration purposes.
    let path = file.path();
    println!("Wrote to: {:?}", path);
    
    // `file` drops here. The OS removes the file immediately.
    Ok(())
}

The wrapper implements std::io::Write and std::io::Read. You can chain standard I/O methods directly. The ? operator propagates errors cleanly. When file reaches the end of the function, Rust calls Drop. The crate issues a delete system call. The file disappears.

What happens under the hood

Creation starts with a call to the operating system. On Unix, tempfile uses mkstemp or open with O_TMPFILE. On Windows, it uses CreateFile with temporary flags. The OS guarantees atomic creation and unique naming. The crate stores the absolute path inside the wrapper struct. It also tracks whether the file should be deleted on drop. This flag starts as true.

When you call write_all, the data goes through the standard library's buffered writer. The buffer sits in user space until flush or Drop pushes it to the kernel. If you read from the file while it is still open, you must seek back to the beginning. The file pointer does not reset automatically after writing.

use std::io::{Read, Seek, Write};
use tempfile::NamedTempFile;

/// Demonstrates seeking and reading from a temporary file.
fn read_back_temp() -> std::io::Result<()> {
    let mut file = NamedTempFile::new()?;
    
    // Writes initial payload to the file.
    file.write_all(b"hello")?;
    
    // Moves the file pointer back to the start.
    // Without this, the next read would return zero bytes.
    file.rewind()?;
    
    let mut buffer = String::new();
    file.read_to_string(&mut buffer)?;
    
    // Prints the recovered data to stdout.
    println!("Recovered: {}", buffer);
    Ok(())
}

The Drop implementation runs synchronously. It does not spawn background threads. It does not schedule async cleanup. The deletion happens on the current thread before the next line of code executes. This deterministic behavior is why tempfile works reliably in tests, compilers, and long running services.

Trust the drop order. If you hold a reference to the path after the wrapper drops, you will get a dangling pointer. Rust prevents this at compile time.

Real-world usage: compilation artifacts

Compilers and build tools generate intermediate files constantly. Link time optimization, incremental compilation, and macro expansion all produce temporary data. The rustc_codegen_gcc project uses tempdir() to store intermediate LTO bitcode files safely during compilation. The pattern scales to any toolchain that needs a scratch space.

use std::fs;
use tempfile::tempdir;

/// Creates a temporary directory for intermediate build artifacts.
/// Returns the directory handle so cleanup happens when the caller drops it.
fn prepare_build_artifacts() -> std::io::Result<tempfile::TempDir> {
    // Allocates a unique directory in the system temp folder.
    // The directory and all its contents delete on drop.
    let dir = tempdir()?;
    
    // Creates a subdirectory for object files.
    fs::create_dir_all(dir.path().join("obj"))?;
    
    // Writes a placeholder config to demonstrate nested paths.
    fs::write(dir.path().join("config.json"), "{}")?;
    
    // Returns the handle. The caller owns the cleanup responsibility.
    Ok(dir)
}

The function returns the TempDir handle. The caller decides when cleanup happens. If the caller stores the handle in a struct, the directory lives as long as the struct. If the caller drops it immediately, the directory vanishes. This explicit ownership matches Rust's philosophy. You control the lifetime by controlling the variable.

Convention aside: keep temporary paths short lived. Do not pass TempDir across thread boundaries unless you explicitly clone the handle or convert it to a persistent path. The crate supports Arc<TempDir> for shared ownership, but most tools prefer keeping the scratch space local to a single function or task.

Where things go wrong

Temporary files fail in predictable ways. The most common mistake is trying to keep the file after the wrapper drops. If you need the file to survive, you must persist it explicitly.

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

/// Writes data and persists the file to a fixed location.
fn save_permanently() -> std::io::Result<()> {
    let mut file = NamedTempFile::new()?;
    file.write_all(b"keep this")?;
    
    // Converts the temporary file into a `TempPath` handle.
    // The file still exists on disk, but cleanup is disabled.
    let temp_path = file.into_temp_path();
    
    // Moves the file to a permanent location.
    // If the target exists, this call will fail.
    temp_path.persist("/tmp/final_output.txt")?;
    
    Ok(())
}

If you skip into_temp_path() and try to access the path after NamedTempFile drops, you will hit a race condition. The file might be gone by the time you read it. The compiler will not catch this if you clone the PathBuf before the drop.

Another frequent error involves borrowing. If you try to move the path out of the wrapper, the compiler rejects you with E0507 (cannot move out of borrowed content). The wrapper owns the path. You can only borrow it with file.path(). If you need a PathBuf, clone it explicitly.

use tempfile::NamedTempFile;

/// Demonstrates path borrowing versus cloning.
fn path_handling_example() -> std::io::Result<()> {
    let file = NamedTempFile::new()?;
    
    // Borrows the path. Valid only while `file` lives.
    let borrowed_path = file.path();
    
    // Clones the path into an owned `PathBuf`.
    // Safe to use after `file` drops, but the file itself will be deleted.
    let owned_path = borrowed_path.to_path_buf();
    
    // `file` drops here. The OS deletes the file.
    // `owned_path` now points to a deleted location.
    Ok(())
}

Permission errors also surface on restricted systems. If the temporary directory is read only, or if disk quotas are exhausted, NamedTempFile::new() returns an std::io::Error. Propagate it with ? or handle it explicitly. Do not unwrap in production code. Unwrapping a temporary file creation failure crashes the process and leaves no diagnostic trail.

Handle the error at the boundary. Let the caller decide whether to retry or abort.

Picking the right temporary type

Use NamedTempFile when you need a single file with a known path and want automatic cleanup on drop. Use TempDir when you need a container for multiple intermediate files or nested directories. Use NamedTempFile::into_temp_path() followed by persist() when the output must survive the function call and move to a fixed location. Use std::env::temp_dir() only when you are managing cleanup manually and accept the risk of orphaned files on panic or early return.

Where to go next