Question
What are the common ways to read a file in Ruby, and what are the benefits and drawbacks of each approach?
For example, one approach is to open a file, read it one line at a time, and then close it:
file_obj = File.new($file_name, "r")
while (line = file_obj.gets)
puts line
end
file_obj.close
Ruby provides several flexible file-reading APIs. When should each one be used, particularly with respect to memory use, readability, and ensuring that the file is closed?
Short Answer
Ruby can read an entire file at once, return its lines as an array, or process it incrementally. The best choice depends mainly on file size and whether you need the full contents in memory. You will also learn why block-based File.open is usually the safest default when working directly with file handles.
Concept
Ruby represents an open file with a File object, which is also an IO object. An IO object supplies methods such as read, gets, each_line, and readlines.
The central decision is eager reading versus streaming:
- Eager reading loads all file content into memory immediately. Examples:
File.readandFile.readlines. - Streaming reads content gradually, usually one line at a time. Examples:
File.foreach,file.each_line, and repeatedfile.gets.
Eager reading is convenient for small files because you can work with all content immediately. Streaming is safer for large files because Ruby does not need to hold the whole file in memory.
A second important concern is resource management. Opening a file uses an operating-system resource called a file descriptor. A file must eventually be closed. Ruby's block form of File.open closes the file automatically, including when an exception occurs, so it is commonly preferred over manually calling close.
Mental Model
Think of a file as a book.
File.readis like photocopying the entire book and placing every page on your desk. It is convenient for a short book, but a large book can fill the room.File.readlinesis like making a stack where each page is a separate item. This is useful when you need to refer to particular pages.File.foreachandeach_lineare like reading one page, acting on it, and then putting it away before taking the next page. Your desk stays clear.getsis like explicitly asking for the next page each time. It gives you maximum control over when reading happens.
The block form of File.open is like borrowing a book from a library with an automatic return system: Ruby returns (closes) it when the block finishes.
Syntax and Examples
Read the entire file as one string
contents = File.read("notes.txt")
puts contents
File.read returns one String. It is concise and works well for small text files, configuration files, templates, and JSON documents.
Read all lines into an array
lines = File.readlines("notes.txt")
lines.each_with_index do |line, index|
puts "#{index + 1}: #{line.chomp}"
end
File.readlines returns an Array of strings. Each line usually retains its ending newline, so chomp is often useful before display or comparison.
Stream a file one line at a time
File.foreach("notes.txt") do |line|
puts line.chomp
end
File.foreach opens the file, yields each line, and closes the file when done. It is an excellent default for large line-oriented text files.
Open a file explicitly with automatic closing
File.open("notes.txt", "r") do |file|
file.each_line do |line|
puts line.chomp
end
end
Use this form when you need an actual file object—for example, to combine reading methods, inspect the current position, or use a custom encoding.
Manually fetch each line with gets
File.open("notes.txt", "r") do |file|
while (line = file.gets)
puts line.chomp
end
end
gets returns the next line, then returns nil at end of file. This is useful when your loop needs detailed control over when it reads the next line.
Read binary data
bytes = File.binread("image.png")
puts bytes.bytesize
Use File.binread for non-text data such as images, archives, and PDFs. It prevents text newline conversion and avoids interpreting the file using a text encoding.
Step by Step Execution
Consider this streaming example:
File.foreach("tasks.txt") do |line|
task = line.chomp
next if task.empty?
puts "Task: #{task}"
end
Suppose tasks.txt contains:
Email client
Deploy application
Execution proceeds as follows:
File.foreachopenstasks.txt.- Ruby reads the first line,
"Email client\n", and assigns it toline. chompremoves the trailing newline, sotaskbecomes"Email client".- The line is not empty, so Ruby prints
Task: Email client. - Ruby reads the next line, which contains only
"\n". - After
chomp,taskis an empty string.nextskips the rest of this loop iteration. - Ruby reads
"Deploy application\n", removes its newline, and prints it. - At end of file,
File.foreachends the loop and closes the file automatically.
Unlike File.readlines, the program holds only the current line (plus small overhead) while processing the file.
Real World Use Cases
- Application configuration: Use
File.readfor a small JSON, YAML, or text configuration file before parsing it. - Log processing: Use
File.foreachto scan a multi-gigabyte server log for errors without exhausting memory. - CSV-like imports: Stream rows one at a time, validate each row, and save valid records to a database.
- Template loading: Read a small email or HTML template into a string with
File.read. - Line-numbered tools: Use
File.readlineswhen a small source file must be edited or accessed by line number. - Binary uploads and checksums: Use
File.binreadwhen raw bytes matter, such as calculating a digest for a downloaded file.
Real Codebase Usage
In production Ruby code, developers usually choose an API that makes resource handling and memory behavior obvious.
Prefer a block when opening a handle
File.open(path, "r", encoding: "UTF-8") do |file|
file.each_line do |line|
process(line)
end
end
The block ensures close is called even if process raises an exception.
Use guard clauses while streaming
File.foreach(path, encoding: "UTF-8") do |line|
record = line.strip
next if record.empty?
next if record.start_with?("#")
import_record(record)
end
This keeps the main processing path simple and avoids deeply nested if statements.
Handle expected file errors
begin
contents = File.read(path, encoding: "UTF-8")
rescue Errno::ENOENT
warn "File not found: #{path}"
rescue Errno::EACCES
warn "Permission denied: #{path}"
end
Do not silently ignore file errors. A missing file and a permission problem usually need different responses.
Select an encoding deliberately
File.foreach(path, encoding: "UTF-8") do |line|
# Process text expected to be UTF-8.
end
When a project has a known file format, specifying its encoding makes failures easier to diagnose and reduces environment-dependent behavior.
Common Mistakes
Forgetting to close a manually opened file
file = File.open("notes.txt", "r")
puts file.read
# file.close is missing
This can leave file descriptors open, especially in a long-running process. Prefer a block:
File.open("notes.txt", "r") do |file|
puts file.read
end
Loading a huge file with File.read or File.readlines
lines = File.readlines("very-large.log")
This creates an array containing every line, which can consume substantial memory. Stream it instead:
File.foreach("very-large.log") do |line|
process(line)
end
Using inconsistent variable names
The original style can accidentally create one variable and use another:
fileObj = File.new($file_name, "r")
while (line = file_obj.gets)
puts line
end
Ruby treats fileObj and file_obj as different variables. Ruby convention also favors snake case:
file_obj = File.new($file_name, "r")
Forgetting that lines include newline characters
File.foreach("names.txt") do |line|
puts "Hello, #{line}!"
end
If line ends in \n, the exclamation mark appears on the next line. Use chomp:
puts "Hello, #{line.chomp}!"
Using text reading for binary content
Avoid treating images, compressed files, or arbitrary bytes as ordinary text. Use File.binread or open with binary mode ("rb") when byte-for-byte preservation matters.
Assuming a file always exists
File.read and related methods raise Errno::ENOENT when the path does not exist. Validate user-provided paths when appropriate and handle errors where the application can make a useful decision.
Comparisons
| Approach | Returns | Memory use | Automatically closes? | Best for |
|---|---|---|---|---|
File.read(path) | One String | High for large files | Yes | Small files needed as complete text |
File.binread(path) | One binary String | High for large files | Yes | Small binary files or raw bytes |
File.readlines(path) | Array of lines | High for large files | Yes | Small files when you need all lines or indexes |
File.foreach(path) { ... } | Iterates lines | Low | Yes | Large text files processed line by line |
File.open(path) { |f| ... } | A File object in the block | Depends on methods used | Yes | Custom reading logic and options |
file.each_line | Iterates lines | Low | Only with block-based File.open | Line processing using an open handle |
file.gets in a loop | One line per call | Low | Only with block-based File.open | Reading controlled by custom loop logic |
File.new(path, "r") and File.open(path, "r") both create file objects. The practical difference is that File.open accepts a block and automatically closes the file afterward. Prefer File.open when you need to open a file yourself.
each_line and gets both stream lines. Choose each_line for straightforward iteration; choose gets when each read must happen at a precise point in a more complex loop.
Cheat Sheet
# Entire small text file
text = File.read(path, encoding: "UTF-8")
# Entire small file as an array of lines
lines = File.readlines(path, encoding: "UTF-8")
# Preferred line-by-line processing
File.foreach(path, encoding: "UTF-8") do |line|
puts line.chomp
end
# Custom work with a safely managed file object
File.open(path, "r", encoding: "UTF-8") do |file|
file.each_line { |line| process(line) }
end
# Explicitly request one line at a time
File.open(path, "r") do |file|
while (line = file.gets)
process(line)
end
end
# Raw binary bytes
bytes = File.binread(path)
- Use
File.readfor small text files. - Use
File.readlinesonly when you really need an array containing every line. - Use
File.foreachfor large line-based files. - Prefer
File.open { |file| ... }over manualopenandclose. getsreturnsnilat end of file.chompremoves a record separator, commonly the trailing newline.- Text and binary files have different needs; use
binreador"rb"for raw bytes.
FAQ
What is the easiest way to read a file in Ruby?
For a small text file, use File.read("path/to/file.txt"). It returns the entire contents as one string.
What is the best Ruby method for reading a large file?
Use File.foreach(path) { |line| ... } to process one line at a time. It avoids loading the complete file into memory.
Does File.readlines close the file automatically?
Yes. File.readlines opens the file internally, reads the lines, and closes it before returning the array.
Should I use File.new or File.open in Ruby?
Prefer File.open with a block. It automatically closes the file when the block ends, including if an exception is raised.
Why does Ruby add a blank line when I print a line from a file?
The line likely already ends with \n, and puts adds another newline. Use puts line.chomp when you want to remove the existing line ending.
When should I use gets instead of each_line?
Use each_line for normal line iteration. Use gets when you need manual control over exactly when the next line is read.
How do I read a binary file in Ruby?
Use File.binread(path) for a small binary file, or File.open(path, "rb") when streaming binary data.
What happens if Ruby cannot find the file?
Ruby raises Errno::ENOENT. Handle it with rescue when your program can recover or show a helpful message.
Mini Project
Description
Build a small Ruby log scanner that reads an application log one line at a time and reports error entries. This demonstrates streaming file processing, newline cleanup, guard clauses, and safe file access without loading the full log into memory.
Goal
Print every non-empty log line containing ERROR, followed by the total number of errors found.
Requirements
Use File.foreach to read the file line by line.
Ignore blank lines.
Match error entries containing the text ERROR.
Print each matching entry without its trailing newline.
Print the final error count.
Handle a missing file with a helpful message.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.