---
title: How to Append to a File in Rust
summary: Append to a file in Rust using OpenOptions with append(true) to add data without overwriting.
lang: en
slug: how-to-append-to-a-file-in-rust
published: 2026-04-17T12:05:42+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-append-to-a-file-in-rust/
---

# How to Append to a File in Rust



## Appending to a file without overwriting

You're building a background service that tracks events. Every time a user logs in, you want to write a timestamp to a log file. You don't want to overwrite yesterday's entries. You just want to tack the new line onto the end. In Python, you'd open the file with mode `'a'`. In Rust, the approach is explicit and safe, but it requires understanding how the standard library handles file modes and why the default behavior is different.

## The tape deck analogy

Think of a file like a magnetic tape. When you open a file for writing in the default mode, the tape head sits at the very beginning. If you write, you overwrite whatever is there. The OS doesn't guess your intent. It assumes you want to replace the content.

Append mode is different. It's like a tape deck with a "fast forward to end" button that triggers automatically the moment you press record. The system moves the cursor to the last byte, and every write operation starts from there. The existing content stays untouched. You can't accidentally overwrite the start of the file because the cursor is locked to the end.

## Minimal example

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

fn main() -> std::io::Result<()> {
    // OpenOptions is a builder. You configure flags, then call open.
    // Nothing happens on the filesystem until open() is called.
    let mut file = OpenOptions::new()
        .create(true) // Create the file if it doesn't exist.
        .append(true) // Seek to end before every write.
        .open("events.log")?;

    // writeln! formats the string and adds a newline.
    // The ? operator propagates IO errors to the caller.
    writeln!(file, "Event recorded at {}", std::time::SystemTime::now())?;

    Ok(())
}
```

## What happens under the hood

When you call `OpenOptions::new()`, you get a configuration struct. Chaining `.create(true)` and `.append(true)` sets internal flags. No file is opened yet. When you call `.open()`, Rust passes those flags to the operating system. The OS opens a file descriptor.

The `append` flag tells the kernel to set the `O_APPEND` mode. This mode changes how write syscalls behave. Normally, a write happens at the current file offset. With `O_APPEND`, the kernel atomically seeks to the end of the file and then writes. This happens inside the kernel, not in your Rust code. You don't need to call `seek` manually. The kernel handles the positioning.

This atomicity is a hidden superpower. If you have multiple threads writing to the same file handle, or even multiple processes opening the same file with append mode, the OS guarantees that writes land at the end. You won't get interleaved bytes. If you tried to replicate this by manually seeking to the end, you'd introduce a race condition. The file could grow between your seek and your write, causing you to overwrite the other thread's data. Append mode solves this at the kernel level.

Trust the atomicity. The kernel handles the positioning.

## Realistic usage with buffering

In a real application, you rarely open and close the file for every line. Opening a file involves system calls and disk metadata lookups. You hold the file handle open and write to it repeatedly. When you write many small chunks, you also want to reduce system call overhead. Every call to `write` on a file triggers a system call. System calls have overhead. If you're logging thousands of lines, that overhead adds up.

Wrapping the file in `BufWriter` allocates a chunk of memory. Writes fill that memory. The actual system call happens only when the buffer fills or when you flush. This can speed up I/O by orders of magnitude for small writes.

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

/// Appends a log entry to the file using a buffered writer.
/// Returns the writer so the caller can keep writing.
fn open_log_writer(path: &str) -> io::Result<BufWriter<std::fs::File>> {
    let file = OpenOptions::new()
        .create(true)
        .append(true)
        .open(path)?;

    // BufWriter wraps the file.
    // Writes go to the buffer first, reducing syscall frequency.
    Ok(BufWriter::new(file))
}

fn main() -> io::Result<()> {
    let mut writer = open_log_writer("app.log")?;

    // write! fills the buffer.
    // No syscall happens until the buffer is full or flushed.
    write!(writer, "Starting application\n")?;
    write!(writer, "Loading config\n")?;

    // flush() pushes the buffer to the OS.
    // The OS may still cache the data, but it's out of your process.
    writer.flush()?;

    Ok(())
}
```

Convention aside: `flush()` pushes data to the OS buffer. `sync_all()` forces the OS to write to the physical disk. For a log file, `flush()` is usually sufficient. If you're writing transaction data where losing a byte on power failure is catastrophic, reach for `sync_all()`.

Buffering is free performance. Wrap small writes in `BufWriter`.

## Pitfalls and compiler errors

If you forget `.create(true)` and the file doesn't exist, `open` returns an error. You'll see a `NotFound` error. The file is not created automatically unless you ask for it. This prevents accidental file creation in wrong directories.

If you forget `mut` on the file variable, you can't write. The compiler rejects this with `E0596` (cannot borrow as mutable). Writing requires a mutable reference because it changes the file state. The compiler enforces this. You must mark the variable as mutable.

If you use `File::create()` instead of `OpenOptions`, you truncate the file. `File::create()` wipes the content. That's a common trap. `File::create()` is equivalent to `OpenOptions::new().write(true).create(true).truncate(true)`. The `truncate` flag sets the file length to zero. If you want to append, never use `File::create()`.

If you try to write a non-string type without formatting, you get `E0277` (trait bound not satisfied). The `write!` macro requires types that implement `Display` or `Debug`. If you pass a raw byte slice or a struct without the trait, the compiler stops you. Use `write!(file, "{:?}", value)` for debug output, or implement `Display` for your types.

If you mix `append(true)` with `write(true)`, the behavior depends on the OS. On most systems, `append` takes precedence for writes. However, relying on this is fragile. Stick to one mode. If you need to write and append, open two handles or use `OpenOptions` with the flags you actually need.

Don't reach for `File::create()` unless you want to nuke the file. Stick to `OpenOptions`.

## When to use append vs alternatives

Use `OpenOptions::append(true)` when you need to add data to the end of a file without reading or overwriting existing content. Use `OpenOptions::write(true)` when you need random access writes or want to overwrite specific parts of the file. Use `File::create()` when you want to create a new file or truncate an existing one to zero length. Use `File::open()` when you only need to read the file and don't intend to modify it. Use `BufWriter` wrapped around an append file when you are writing many small chunks and want to reduce system call overhead. Use `OpenOptions::create_new(true)` when you must ensure the file is fresh and fail if it already exists.

Pick the tool that matches your intent. The compiler will thank you for being explicit.

## Where to go next

- [How to Use the dirs Crate for Platform-Specific Directories](/en/how-to-use-the-dirs-crate-for-platform-specific-directories/)
- [How to Handle Platform-Specific File Paths in Rust](/en/how-to-handle-platform-specific-file-paths-in-rust/)
- [How to Handle File Permissions in Rust](/en/how-to-handle-file-permissions-in-rust/)