In Ruby, why is File.open with a block preferred over opening and closing the file yourself, and what does the block form return?
answer
- who owns the handle
- closes on normal exit and on raise
- returns the block's value
- no block: returns the File
- Errno::EMFILE, Too many open files
basics
~20 sFile.open with a block yields the File, closes it when the block exits, even through an exception, and returns the block's value. Without a block it returns the File, and the caller must close it or leak the descriptor.
solid answer
~40 s`File.open(path) { |f| ... }` yields the open `File`, closes it in an ensure-style cleanup whether the block returns normally, `break`s or raises, and then returns whatever the block returned. Without a block it returns the `File` object and the caller owns it: an exception between the open and the `close` skips the close, and the descriptor stays open until the garbage collector happens to reclaim the object. In a long-running process that leaks descriptors until the per-process limit is reached and `File.open` raises `Errno::EMFILE` (Too many open files). So the block form is the default; the no-block form is for a handle that must outlive one method, paired with `begin ... ensure f.close end`.
code
ruby · 20 lines# Block form: returns the block's value; the file is already closed
first_line = File.open("app.log") { |f| f.gets }
handle = nil
begin
File.open("app.log") do |f|
handle = f
raise ArgumentError, "bad record"
end
rescue ArgumentError
handle.closed? # => true, closed before the error reached us
end
# No block: the caller owns the File and must close it
f = File.open("app.log")
begin
header = f.gets
ensure
f.close
endgo deeper
Recall that the block form closes the file for you on every exit and returns the block's value; reach for it by default.
Explain that the block form is an ensure around the block, why an exception or early return leaks a no-block handle, and what IOError you get from a closed File.
Connect leaked handles to Errno::EMFILE in long-running workers, know that GC finalization closes files only eventually, and show the begin/ensure pattern for handles that outlive a method.
Frame resource ownership as an API design rule: expose block-taking methods that own cleanup, so callers of your library cannot leak descriptors by forgetting a close.
## Two ways to call File.open Ruby's `File.open(path, mode = "r")` has two shapes, and they differ in **who is responsible for closing the file**. - **Block form** — `File.open("app.log") { |f| f.gets }`. Ruby opens the file, yields the `File` object to the block, and closes it when the block finishes. The method returns **the block's value**, not the file. - **No-block form** — `f = File.open("app.log")`. Ruby returns the open `File` object. The caller owns it and must call `f.close`. The rdoc for `File.open` in `io.c` says exactly this: with no block it returns the File object; with a block it calls the block with the File and returns the block's value. In the C source the block form is implemented as `rb_ensure(rb_yield, io, io_close, io)` — the close is registered as an *ensure* step, so it runs however the block exits. ## What the block form guarantees 1. The file is closed when the block returns normally. 2. The file is closed when the block raises; the exception then propagates to the caller unchanged. 3. The file is closed when the block exits early through `break`, `next` or a `return` from the enclosing method. 4. `File.open` returns the block's last value, so `data = File.open(path) { |f| f.read }` hands back the contents with no open handle left behind. The `File` object itself survives the block if you smuggle it out, but it is closed: `f.closed?` returns `true`, and reading from it raises `IOError` (closed stream). ## What goes wrong with the no-block form The no-block form is not wrong, but it makes cleanup your job, and the usual mistakes are: - **An exception between open and close.** `f = File.open(p); parse(f.read); f.close` never reaches `close` if `parse` raises. - **Early returns.** A guard clause that returns before `close` leaves the handle open. - **Relying on the garbage collector.** Ruby does close a stream when the GC reclaims its `File` object, but *when* that happens is not under your control. Until then the descriptor stays open, and buffered writes may not have reached the file. - **Descriptor exhaustion.** Every process has a limit on open file descriptors. A worker that opens files in a loop without closing them eventually fails in `File.open` with `Errno::EMFILE` — the kernel's "Too many open files" error — often far from the code that leaked. When a handle really must outlive a single method (a log writer kept open for the life of an object, say), write the cleanup explicitly: ```ruby f = File.open("export.csv", "w") begin rows.each { |row| f.puts(row.join(",")) } ensure f.close end ``` ## Block form and the one-shot class methods For whole-file work you often do not need a handle at all. `File.read(path)`, `File.write(path, data)`, `File.readlines(path)` and `File.foreach(path) { ... }` each open the file, do their work and close it before returning. They are shorthand for the block form; choosing between them is about memory and access pattern, not about closing. | Call | Returns | Who closes | |---|---|---| | `File.open(path) { \|f\| ... }` | the block's value | Ruby, on any exit | | `File.open(path)` | the open `File` | you, with `close` | | `File.read(path)` | the contents as a `String` | Ruby, before returning | | `File.write(path, data)` | bytes written (`Integer`) | Ruby, before returning | ## Interview traps - Saying the block form returns the file. It returns the block's value. - Saying the file stays open if the block raises. The close runs first, then the exception continues. - Saying Ruby closes a file "when the variable goes out of scope". Ruby has no scope-based destruction; an explicit `close`, the block form, eventual garbage collection or process exit is what closes it. - Forgetting that a closed `File` object still exists — using it afterwards raises `IOError`, not `NoMethodError`. A good answer names the guarantee (closed on every exit path), the return value (the block's), and the failure it prevents (`Errno::EMFILE` in a long-running process).
- What happens if you keep the File yielded by File.open and call read on it after the block ends?The Ruby object still exists, but its descriptor was closed when the block finished, so `f.closed?` is `true` and `f.read` raises `IOError` (closed stream). If the data is needed later, return it from the block, as in `data = File.open(path) { |f| f.read }`, instead of leaking the handle out.
- Is a File you forgot to close ever closed at all?Yes, eventually: Ruby closes a stream when the garbage collector reclaims its `File` object. You do not control when that happens, so a loop that opens files faster than objects are collected can still hit the descriptor limit and raise `Errno::EMFILE`, and buffered writes may sit unflushed until the object is finalized. It is a delayed leak, not cleanup.
- Do File.read, File.write and File.foreach need a block or an explicit close?No. Each opens the path, does its work and closes the stream before returning; `File.foreach` with a block closes when iteration ends or the block raises. Without a block, `File.foreach` returns an `Enumerator` that only opens the file when something iterates it.
The block form is a hotel key card that stops working at checkout, whether you leave calmly or run out during a fire alarm. The no-block form is a metal key you must remember to hand back; if you forget, the hotel only notices much later, and meanwhile the room stays taken.
saying these in an interview costs you the question
- The block form only closes the file if the block finishes without raising.
- File.open with a block returns the File object so you can keep reading it later.
- Ruby closes a file as soon as the variable holding it goes out of scope.
- You never need close because the garbage collector cleans up files promptly.
- File.read leaves the file open until you close it yourself.