NodeJSReading Files

Reading Files

Reading a file is the most common filesystem task, and Node gives you several tools for it — readFile for whole files, streams for large ones, and readline for line-by-line processing. Choosing correctly comes down to one question: how big is the file, and do you need it all at once?

Read an entire file

JS
import { readFile } from 'node:fs/promises'

// As text — pass an encoding:
const text = await readFile('notes.txt', 'utf8')

// As raw bytes — omit the encoding to get a Buffer:
const bytes = await readFile('photo.jpg')
console.log(text.length, 'chars;', bytes.length, 'bytes')
Encoding decides string vs Buffer
With `'utf8'` (or `'ascii'`, `'base64'`, …) you get a decoded **string**. With no encoding you get a **Buffer** of raw bytes — correct for images, PDFs, or any binary format where decoding to text would corrupt the data.
Reading and parsing JSON

A constant need. Read the text, then JSON.parse — and wrap it so a malformed file gives a clear error instead of a cryptic one:

JS
import { readFile } from 'node:fs/promises'

async function readJSON(path) {
  const text = await readFile(path, 'utf8')
  try {
    return JSON.parse(text)
  } catch {
    throw new Error(`Invalid JSON in ${path}`)
  }
}

const config = await readJSON('config.json')
For static JSON, import assertions are an option
In modern ESM you can `import data from './data.json' with { type: 'json' }` — loaded once, cached, no `fs` call. Use that for *static* JSON bundled with your code; use `readFile`+`JSON.parse` for files that change at runtime or whose path is dynamic.
Handling the missing-file case
Distinguish ENOENT from real errors
Don't catch-all every error as "file missing". Check `err.code === 'ENOENT'` for not-found, `'EACCES'` for permissions, and re-throw anything you didn't expect — swallowing unknown errors hides real bugs.

JS
import { readFile } from 'node:fs/promises'

async function readOrDefault(path, fallback) {
  try {
    return await readFile(path, 'utf8')
  } catch (err) {
    if (err.code === 'ENOENT') return fallback   // expected: use default
    throw err                                     // unexpected: propagate
  }
}
Large files: stream, don't buffer
`readFile` on a huge file exhausts memory
`readFile` holds the *entire* file in memory at once. A 2 GB file needs 2 GB of RAM — and several concurrent reads can crash the process. For large files, use `createReadStream`, which delivers the data in small chunks with constant memory use.

JS
import { createReadStream } from 'node:fs'

const stream = createReadStream('huge.log', { encoding: 'utf8' })

stream.on('data', (chunk) => process.stdout.write(chunk))   // chunk by chunk
stream.on('end', () => console.log('\n--- done ---'))
stream.on('error', (err) => console.error('read failed:', err.code))

Streams are covered fully in Readable Streams.

Line by line with readline

To process a big text file one line at a time — logs, CSVs — combine a read stream with readline and for await. This never holds more than one line in memory:

JS
import { createReadStream } from 'node:fs'
import { createInterface } from 'node:readline'

const rl = createInterface({
  input: createReadStream('access.log'),
  crlfDelay: Infinity,            // treat \r\n as one line break
})

let count = 0
for await (const line of rl) {
  if (line.includes('ERROR')) count++
}
console.log(`${count} error lines`)
Choosing the right tool

Situation

Use

Small file, need it all

readFile

Static JSON bundled with code

import … with { type: "json" }

Large file, process in chunks

createReadStream

Large text, work line by line

readline + for await

Copy file to file

pipeline() of read → write streams

Next
The flip side — creating and overwriting files safely: [Writing Files](/nodejs/writing-files).