In Ruby, why can code that checks File.exist? before calling File.open still raise Errno::ENOENT, and what should it do instead?
answer
- check, then act: two steps
- another process deletes in between
- rescue Errno::ENOENT around the open
- wx raises Errno::EEXIST
- File.exists? removed in 3.2
basics
~20 sFile.exist? and File.open are two separate system calls, and another process can delete or move the file between them. Open directly and rescue Errno::ENOENT; to create only if absent, open with wx and rescue Errno::EEXIST.
solid answer
~40 s`File.exist?(path)` only reports what `stat` saw at that instant. Between that check and `File.open`, a log rotator, a cleanup job or another worker can delete or rename the file, so the open raises `Errno::ENOENT` anyway — a check-then-act race. The robust pattern is to **attempt the operation and handle its failure**: `File.open(path) { ... } rescue Errno::ENOENT`. For create-if-absent, open with `"wx"`, which checks and creates in one call and raises `Errno::EEXIST` if the file is there. The same caution applies to `File.size` and `File.mtime`, which raise `Errno::ENOENT` on a missing path, while `File.size?` returns `nil` for a missing or empty file. On Ruby 4.0 the predicate is `File.exist?`; `File.exists?` was removed in 3.2.
code
ruby · 18 lines# Racy: the file can vanish between the check and the open
# File.open(path) { |f| import(f) } if File.exist?(path)
def import_if_present(path)
File.open(path) { |f| f.each_line(chomp: true).count }
rescue Errno::ENOENT
0
end
def claim(marker)
File.open(marker, "wx") { |f| f.write(Process.pid.to_s) }
true
rescue Errno::EEXIST
false
end
File.size?("empty.txt") # => nil for an empty or missing file
File.mtime("app.log") # => a Time; raises Errno::ENOENT if missinggo deeper
Recall File.exist?, File.size and File.mtime, and that File.open on a missing path raises Errno::ENOENT.
Explain why exist-then-open is a race, rewrite it as open plus rescue Errno::ENOENT, and contrast File.size with File.size?.
Spot check-then-act races around rotated logs and shared directories, and use wx with Errno::EEXIST for markers and lock files.
Establish attempt-and-handle as the team pattern for shared file systems and reserve pre-checks for advisory user-facing messages.
## The check-then-act race A common pattern looks safe: ```ruby if File.exist?(path) File.open(path) { |f| import(f) } end ``` It is not. `File.exist?` asks the operating system whether a `stat` of the path succeeds **at that moment**, and `File.open` asks it again later. Those are two separate system calls, and the file system is shared with every other process on the machine. Between them: - a log rotator can rename `app.log` to `app.log.1`; - a cleanup job can delete an expired upload; - a second worker processing the same queue can move the file to an archive. When that happens, `File.open` raises `Errno::ENOENT` ("No such file or directory") even though the check just passed. The bug shows up rarely and only under load, which is why it survives code review. The general name is **time-of-check to time-of-use**. ## Attempt, then handle the failure The fix is to make the operation itself the check: ```ruby begin File.open(path) { |f| import(f) } rescue Errno::ENOENT logger.info("skipped #{path}: already gone") end ``` Now there is exactly one system call, and its failure is handled. The `File.exist?` call was not protecting anything; it was only making the failure rarer. ## Creating a file only if it is absent The mirror-image race is "if it does not exist, create it": two processes can both see *absent* and both write. The exclusive-create mode closes it: 1. Open with `"wx"` (or the integer flags `File::WRONLY | File::CREAT | File::EXCL`). 2. The operating system checks and creates in one step. 3. If the file already exists, Ruby raises `Errno::EEXIST`; exactly one caller wins. This is the standard way to take a lock file or write a "processed" marker. ## The other file queries | Call | Existing file | Missing file | |---|---|---| | `File.exist?(path)` | `true` (also for directories) | `false` | | `File.file?(path)` | `true` for a regular file | `false` | | `File.size(path)` | size in bytes | raises `Errno::ENOENT` | | `File.size?(path)` | size, or `nil` if zero bytes | `nil` | | `File.mtime(path)` | modification `Time` | raises `Errno::ENOENT` | Points worth saying in an interview: - `File.exist?` is also true for a **directory**; use `File.file?` when you need a regular file. - `File.size?` folds "missing" and "empty" into `nil`, handy for "is there anything to process?" - `File.size` and `File.mtime` race exactly like `File.exist?` — they are also a `stat` that can fail. - On an open `File`, `f.size` and `f.mtime` query the handle you already hold, so they describe the file you are reading even if the path has since been renamed. ## Rescue the specific error you expect Once the open itself is the check, rescue precisely. A missing file is `Errno::ENOENT`, but other failures mean different things and usually should not be swallowed the same way: - `Errno::EACCES` — the file exists but the process lacks permission; usually a deployment problem to surface, not skip. - `Errno::EISDIR` — `File.read` was pointed at a directory; a bug in how the path was built. - `Errno::EEXIST` — an exclusive create (`"wx"`) lost the race; often the expected "someone else has it" signal. Writing `rescue Errno::ENOENT` rather than a broad `rescue` keeps the race handled while permission and path bugs still fail loudly. ## When a pre-check is still fine Checks are fine when they are **advisory**: skipping obviously absent optional config, printing a friendly message in a CLI, or deciding between two code paths where either outcome is safe. What they cannot do is guarantee that the next call will succeed. ## Version note `File.exists?` (with an s) was deprecated for years and **removed in Ruby 3.2**, along with `Dir.exists?`. On Ruby 4.0, calling it raises `NoMethodError`; the method is `File.exist?`.
- What is the difference between File.size and File.size? in Ruby?`File.size(path)` returns the size in bytes and raises `Errno::ENOENT` if the path is missing. `File.size?(path)` returns the size, or `nil` when the file is missing or has zero bytes, which makes it a convenient truthy test for "is there anything here to process".
- What happens on Ruby 4.0 if old code calls File.exists?(path)?It raises `NoMethodError`. `File.exists?` and `Dir.exists?` were deprecated aliases and were removed in Ruby 3.2; the methods are `File.exist?` and `Dir.exist?`.
saying these in an interview costs you the question
- Checking File.exist? first guarantees the following File.open will succeed.
- File.exist? returns false for directories, so it proves the path is a file.
- File.size returns nil when the file is missing.
- File.exists? is the preferred modern spelling of File.exist?.
- Two workers cannot both create a file if each checks File.exist? first.