skip to content

In PHP's fopen(), what do the modes 'r', 'w', 'a', 'x' and 'c' do, and what does adding '+' change?

level: juniorimportance: should knowfreq 50%

answer

  1. what happens to the existing content
  2. which modes create a missing file
  3. w truncates, a writes at the end
  4. x is O_EXCL|O_CREAT
  5. adds the other direction

basics

~20 s

'r' reads an existing file; 'w' creates or truncates for writing; 'a' creates or opens and always writes at the end; 'x' creates only when the file is absent; 'c' creates or opens without truncating. '+' adds reading or writing.

solid answer

~40 s

The mode string decides three things: whether you may read, write or both, what happens when the file is missing, and what happens to existing content. `'r'` opens an existing file for reading and fails if it is missing. `'w'` creates the file or **truncates** it to zero length at open time. `'a'` creates or opens and every write goes to the end, whatever `fseek()` says. `'x'` creates the file and fails with `false` plus an `E_WARNING` if it already exists, which maps to `O_EXCL|O_CREAT`. `'c'` creates or opens without truncating and puts the pointer at the start, which is what you want before taking a lock. Adding `'+'` (`'r+'`, `'w+'`, `'a+'`, `'x+'`, `'c+'`) allows both reading and writing and otherwise keeps the base mode's rules.

go deeper

for a junior

Recall the five base modes, which ones create a missing file, and that 'w' truncates while 'a' appends.

for a middle

Explain the pointer rules: where each mode starts, why fseek() does not move writes in 'a', and how '+' changes direction only.

for a senior

Show you choose 'x' for atomic creation and 'c+' for locked updates, because the wrong mode loses data before any lock is taken.

for a principal

Treat mode choice as part of a file-format contract: document which writers may truncate, append or create, so concurrent jobs cannot destroy each other's output.

## What the mode string controls The second argument of `fopen($filename, $mode)` is a short string that answers three questions at once: 1. **Direction** — may you read, write, or both? 2. **Missing file** — does `fopen()` create it or fail? 3. **Existing content** — is it kept, truncated, or does its presence make the call fail? It also sets where the **file pointer** starts, which decides where the first `fgets()` reads and the first `fwrite()` writes. ## The five base modes and their '+' variants | Mode | Reads | Writes | File missing | File exists | Pointer starts | |---|---|---|---|---|---| | `'r'` | yes | no | fails | kept | start | | `'r+'` | yes | yes | fails | kept | start | | `'w'` / `'w+'` | `'w+'` only | yes | created | **truncated to 0 bytes** | start | | `'a'` / `'a+'` | `'a+'` only | yes | created | kept | end; writes always append | | `'x'` / `'x+'` | `'x+'` only | yes | created | **fopen() fails** | start | | `'c'` / `'c+'` | `'c+'` only | yes | created | kept, not truncated | start | Failure means `fopen()` returns `false` and emits an `E_WARNING` such as *Failed to open stream: No such file or directory*. It does not throw, so the caller must check `=== false`. ## Behaviour that trips people up - **`'w'` truncates at open time.** The data is gone the moment `fopen()` returns, before a single `fwrite()`, before any lock and even if the script dies right after. - **`'a'` ignores the pointer for writes.** The manual states that in `'a'` mode `fseek()` has no effect and writes are always appended; in `'a+'`, `fseek()` moves only the reading position. - **`'r+'` overwrites in place.** Writing at the start of an `'r+'` handle replaces bytes; it does not insert them, and it does not shorten the file. Use `ftruncate()` if the new content is shorter. - **`'x'` is an atomic create.** It corresponds to `O_EXCL|O_CREAT` in `open(2)`: the existence check and the creation happen in one system call, so when several processes race, exactly one wins. A `file_exists()` check followed by `fopen($path, 'w')` has a gap between the two calls where another process can slip in. - **`'c'` exists for locking.** It opens without truncating so you can call `flock()` first and truncate only once you hold the lock. ## Extra letters - `'b'` forces binary mode and `'t'` asks for Windows text translation of `\n` to `\r\n`. The default translation mode is `'b'`, and on Unix-like systems the two behave the same; the manual recommends `'b'` for portability. Put either at the end: `'rb'`, `'wb'`. - `'e'` sets close-on-exec on the descriptor, so a child process started from the script does not inherit it (POSIX builds only). ## A short example ```php <?php declare(strict_types=1); // Create a report exactly once, even if two workers run at the same moment. $h = @fopen('/var/reports/2026-09.csv', 'x'); if ($h === false) { return; // someone else created it first } fwrite($h, "id,total\n"); fclose($h); ``` The `@` only hides the expected warning when the file already exists; the `=== false` check is what drives the logic. ## When fopen() fails Whatever the mode, a failed open returns `false` and emits an `E_WARNING`; it does not throw unless the application installs an error handler that converts warnings into `ErrorException`. Typical causes are a missing file for `'r'`/`'r+'`, an existing file for `'x'`, a missing parent directory for the creating modes, or missing permissions. The manual also notes that `fopen()` may succeed when the path is a directory, so code that accepts arbitrary paths should check `is_dir()` first. Passing the `false` on to `fgets()` or `fwrite()` throws a `TypeError`, which is a confusing place to discover the real problem. ## Choosing a mode - Reading only: `'r'` (or `'rb'`). - Replacing a whole file you own: `'w'`, or better, write elsewhere and move it into place. - Logs and journals: `'a'`, because every write lands at the end. - Create-once files, sentinels, lock files: `'x'`. - Read-modify-write under a lock: `'c+'`, then `flock()`, then read, `ftruncate()`, `rewind()` and write.

  • How does mode 'x' let several processes create a file exactly once?
    It maps to `O_EXCL|O_CREAT`, so the check for an existing file and the creation are one atomic system call. When several processes race, one gets a handle and the rest get `false` plus an `E_WARNING`. The tempting alternative, `file_exists()` followed by `fopen($path, 'w')`, leaves a window between the two calls in which two processes can both see "missing" and both write.
  • You write a shorter string over the start of a file opened with 'r+'; what does the file contain afterwards?
    The new bytes replace the first bytes of the file, and everything after them stays, so the tail of the old content survives. 'r+' neither inserts nor truncates. Call `ftruncate($h, ftell($h))` after writing, or `ftruncate($h, 0)` plus `rewind($h)` before writing, to get only the new content.

saying these in an interview costs you the question

  • 'w' keeps the existing content until the first fwrite() call.
  • In 'a+' mode you can fseek() back and overwrite earlier bytes.
  • 'r+' creates the file when it does not exist.
  • 'x' simply opens the file when it already exists.
  • Without 'b', binary data is corrupted on Linux.