Efficiently read files in Node.js with the fs module
Use readFile() from node:fs/promises when you need a small file’s complete contents. For a large
file, process a stream one chunk at a time. The examples below read the same input as text, count
its bytes, and inspect its first ten bytes without changing the file.
Set up a sample file
These examples were tested with Node.js v26.8.2 on macOS. The setup uses Bash; no packages need to
be installed. Save the JavaScript examples with the .mjs filenames shown so Node.js
loads them as ES modules,
including inside a CommonJS project.
Run this in a directory where you want to create the demo:
mkdir node-read-demo &&
cd node-read-demo &&
printf 'Hello, 🌍!\n' > example.txt
The setup refuses an existing node-read-demo directory. If either mkdir or cd fails, the
&& chain stops before writing example.txt. Stop on failure and choose a fresh location. After
success, stay in node-read-demo and save each script there. The scripts only print results to the
terminal; rerunning them leaves your input unchanged.
Read a small UTF-8 file
Save this as read-text.mjs:
import { readFile } from 'node:fs/promises'
async function main() {
const path = process.argv[2] ?? 'example.txt'
const text = await readFile(path, 'utf8')
process.stdout.write(text)
}
main().catch((error) => {
console.error(`Read failed: ${error.code ?? 'UNKNOWN'}`)
process.exitCode = 1
})
Run it from the demo directory:
node read-text.mjs example.txt
The output is Hello, 🌍! followed by a newline, exactly as stored in the file. The optional
argument defaults to example.txt. Relative input paths are resolved from the terminal’s current
working directory, not the script’s directory.
The 'utf8' argument makes readFile()
return a string. Omit it when you need a Buffer containing the original bytes, such as an image
or an archive. UTF-8 decoding is for text, not arbitrary binary data.
Process a large file without collecting it
Although readFile() is asynchronous, it still loads the entire result into memory. Use it when
that fits your application’s memory budget, including simultaneous reads. Promise.all() over an
unbounded list of files can retain many complete results at once.
A stream lets you consume and discard chunks. Save this as count-bytes.mjs to count the bytes
actually read:
import { createReadStream } from 'node:fs'
async function main() {
const path = process.argv[2] ?? 'example.txt'
let total = 0
for await (const chunk of createReadStream(path)) {
total += chunk.length
}
console.log(`Read ${total} bytes.`)
}
main().catch((error) => {
console.error(`Read failed: ${error.code ?? 'UNKNOWN'}`)
process.exitCode = 1
})
node count-bytes.mjs example.txt
This prints Read 13 bytes. The globe occupies four UTF-8 bytes, so counting characters would give
a different answer. No encoding is set on the stream: each chunk is a Buffer of raw bytes. An
empty file prints Read 0 bytes.
The for await...of loop consumes
the stream and propagates read failures to catch. The file stream closes its descriptor on
completion or error by default. This example retains a counter instead of all the chunks; appending
every chunk to an array or string would lose that memory benefit.
To adapt the loop for sequential processing, do the work inside it and await asynchronous work
before continuing. A chunk is not necessarily a complete line, JSON object, or CSV record. Use a
parser that handles boundaries if your task needs those records. If you only need a file’s reported
size, stat() avoids reading its contents at all.
Read only the first ten bytes
For inspecting a binary header, open a FileHandle and read a bounded prefix. Save this as
read-prefix.mjs:
import { open } from 'node:fs/promises'
async function main() {
const path = process.argv[2] ?? 'example.txt'
const handle = await open(path, 'r')
try {
const buffer = Buffer.alloc(10)
let total = 0
while (total < buffer.length) {
const { bytesRead } = await handle.read(buffer, total, buffer.length - total, total)
if (bytesRead === 0) break
total += bytesRead
}
console.log(`Read ${total} bytes: ${buffer.subarray(0, total).toString('hex')}`)
} finally {
await handle.close()
}
}
main().catch((error) => {
console.error(`Read failed: ${error.code ?? 'UNKNOWN'}`)
process.exitCode = 1
})
node read-prefix.mjs example.txt
This prints Read 10 bytes: 48656c6c6f2c20f09f8c. The last argument to
handle.read() is the
file position; here it advances from zero. A read may return fewer bytes than requested, so the
loop continues until it fills ten bytes or reaches EOF (bytesRead === 0). finally closes the
handle even if reading fails.
The subarray() limits the output to
bytes actually read, excluding unused space for a short or empty file. Hex output also avoids
decoding an incomplete UTF-8 character: this ten-byte prefix ends partway through the globe.
Diagnose a failed read
Try a filename that does not exist:
node read-text.mjs missing.txt
This prints Read failed: ENOENT to standard error and exits with status 1. All three scripts
report read failures this way. Setting
process.exitCode marks failure while
allowing pending output to finish.
For ENOENT, check the input path and working directory. EACCES indicates a permissions problem;
check that the process can traverse the parent directories and read the file. Attempt the read
directly instead of checking with access() first: Node.js documents the
race between checking and opening.
Fit the API to existing code
The callback form of fs.readFile()
is still supported. In callback-based code, inspect the first error argument before using the
data; you do not need to convert surrounding code to promises just for a read.
fs.readFileSync() blocks JavaScript execution
until it finishes. It can suit a short command-line script or startup configuration load. Keep
blocking reads out of request handlers that must serve other clients while disk I/O is pending.
