Table of Contents
Node.js Guide
How to Count Lines in a File with Node.js (readline, Streams, and the Modern for await...of Pattern)
Count lines in a file with Node.js using readFileSync, readline, fs.createReadStream, and manual Buffer scanning. Covers trailing newline edge cases, for await...of, callback versus Promise tradeoffs, and large-file streaming in Node.js 22+.
Node.js gives you three practical ways to solve node js count lines in file:
- read the whole file and split it
- stream it through
readline - scan raw bytes from
createReadStream
Most tutorials stop at option 1:
import { readFileSync } from 'node:fs';
const count = readFileSync('file.txt', 'utf8').split('\n').length;
That is easy to type, but it has two real problems:
node js split newline countover-counts when the file ends with a trailing newline- the whole file is loaded into memory, which is the wrong answer for
node js count lines large file
The modern default is node js readline count lines with for await...of:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('file.txt'),
crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
count++;
}
That is the cleanest node js count lines async pattern for most real applications.
There is one more nuance that matters if you care about speed. The current Node.js docs say two things that are easy to miss:
- callback-based
fsAPIs are still preferable when maximal performance matters readlineasync iteration is slower than the traditional'line'event API
So this guide separates the real jobs:
node js count lines in filefor small filesnode js readline count linesfor readable streamingnode js count lines asyncwithfor await...ofnode js createreadstream count linesfor maximum throughputnode js split newline countedge cases with empty files and trailing newlines
If you are comparing plain JavaScript and typed Node code side by side, the companion TypeScript file reading guide covers the same stream patterns plus the Buffer-versus-string type trap.
30-Second Cheat Sheet
- Small file, simple script:
readFileSync(path, 'utf8')plus corrected line counting - Small file, async code:
await readFile(path, 'utf8')plus corrected line counting - Streaming default:
readline.createInterface()plusfor await...of - Faster readline path:
'line'event plusonce(rl, 'close') - Fastest general stream count:
createReadStream()plus rawBuffernewline scanning - Windows CRLF input:
crlfDelay: Infinity
If your main goal is node js count lines in a production app, readline is the readable default and raw byte scanning is the performance default.
Method 1: readFileSync and the split('\n') Trap
This is the easiest node js count lines in file pattern:
import { readFileSync } from 'node:fs';
function countLinesSync(filePath) {
const content = readFileSync(filePath, 'utf8');
return content.split('\n').length;
}
It works for demos, but the count is not stable.
Examples:
'a\nb'.split('\n').length; // 2
'a\nb\n'.split('\n').length; // 3
''.split('\n').length; // 1
That is the core node js split newline count bug:
- trailing newline adds an empty final element
- empty file becomes one empty string instead of zero logical lines
Locally on Node.js 22.22.1, I verified:
"a\nb"->2"a\nb\n"->3""->1
Correct the split-based version
For small files, make the last-line rule explicit:
import { readFileSync } from 'node:fs';
function countLinesSync(filePath) {
const content = readFileSync(filePath, 'utf8');
if (content.length === 0) {
return 0;
}
const lines = content.split('\n');
return lines[lines.length - 1] === '' ? lines.length - 1 : lines.length;
}
This is acceptable for a small trusted file.
A safer cross-platform string helper
If you want one string helper that works across \n, \r\n, and bare \r:
function countLinesText(text) {
if (text.length === 0) return 0;
const newlineMatches = text.match(/\r\n|\r|\n/g) ?? [];
const endsWithNewline = /\r\n|\r|\n$/.test(text);
return newlineMatches.length + (endsWithNewline ? 0 : 1);
}
That fixes the node js split newline count issue without requiring a streaming rewrite.
Why this still is not the best general answer
Even the corrected version is still a full-file read.
That means:
- memory grows with file size
- large files create allocation pressure
node js count lines large filebecomes a bad fit
Use it for files under roughly 10MB to 50MB, one-off scripts, or places where clarity matters more than scalability.
If you are solving the same problem in browser-side JavaScript, the JavaScript split newline guide covers the same string edge cases in more detail.
Small-file async variant
If you want node js count lines async without switching to streams yet, the small-file Promise version is straightforward:
import { readFile } from 'node:fs/promises';
async function countLinesAsync(filePath) {
const content = await readFile(filePath, 'utf8');
return countLinesText(content);
}
This is fine when the file is small. It is still a full-file read, so it is not the right default for node js count lines large file.
Method 2: readline Plus for await...of
This is the clean modern node js readline count lines pattern:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function countLines(filePath) {
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
count++;
}
return count;
}
This is the most readable node js count lines async answer for large files because:
- it streams instead of loading the whole file
- it works naturally with
asyncandawait crlfDelay: Infinitytreats\r\nas one line break- it counts logical lines instead of making you reason about byte chunks
Why crlfDelay: Infinity matters
The Node.js docs use this exact option in the read-file-line-by-line example and explain why:
- it recognizes all
\r\npairs as a single line break - it is a good default for file reading
Locally, I verified that CRLF file input produced clean lines "a" and "b" with this option.
The last-line behavior that makes readline attractive
This is where node js readline count lines beats the naive split approach.
The Node.js docs explicitly say the 'line' event is also emitted when a stream ends without a final end-of-line marker.
That means both of these count as 2:
a
b
a
b
Locally on Node.js 22.22.1, I verified:
- empty file ->
0 "a\nb"->2"a\nb\n"->2
That is why readline is such a strong baseline for node js count lines in file.
The version baseline
rl[Symbol.asyncIterator]() was added in Node.js v11.4.0 and v10.16.0, and its support stopped being experimental in later releases.
So node js for await of lines is not brand-new anymore, but it is still the modern style most developers want to write.
Method 3: for await...of Versus the 'line' Event
This is the performance caveat the Node.js docs state directly:
for await...ofis convenient- it is slower than the traditional
'line'event API
The docs even show the mixed approach for people who want async control flow without paying the full iterator overhead.
Event-based readline
import { once } from 'node:events';
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
async function countLinesEvent(filePath) {
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
rl.on('line', () => {
count++;
});
await once(rl, 'close');
return count;
}
This is still a clean node js count lines async function, but it follows the performance-sensitive guidance from the docs more closely.
readline/promises
Node.js also ships a Promises API:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline/promises';
async function countLinesPromises(filePath) {
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
count++;
}
return count;
}
readline/promises.createInterface() was added in Node.js v17.0.0.
This is a nice fit when the rest of your codebase already leans on Promise-based APIs, but it does not remove the underlying iterator cost.
The callback-versus-Promise nuance
The current node:fs docs say callback-based fs APIs are preferable over promise APIs when maximal performance in execution time and memory allocation is required.
That official note is about the fs module broadly, not specifically a guarantee that every callback-shaped line counter always wins. But it is still relevant to node js count lines large file because:
- the stream itself comes from
node:fs - the callback-style event APIs are usually the lower-overhead path
So the practical rule is:
- normal code: use Promise-based patterns freely
- hot path: prefer streams first, then benchmark event-driven code
Method 4: createReadStream Plus Raw Byte Scanning
If all you need is the count, not the line content, this is the strongest node js createreadstream count lines pattern:
import { createReadStream } from 'node:fs';
function countLinesFast(filePath) {
return new Promise((resolve, reject) => {
const stream = createReadStream(filePath);
let count = 0;
let sawAnyByte = false;
let lastByte = 0x0a;
stream.on('data', (chunk) => {
sawAnyByte = true;
for (let i = 0; i < chunk.length; i++) {
if (chunk[i] === 0x0a) {
count++;
}
}
lastByte = chunk[chunk.length - 1];
});
stream.on('end', () => {
if (!sawAnyByte) {
resolve(0);
return;
}
resolve(lastByte === 0x0a ? count : count + 1);
});
stream.on('error', reject);
});
}
Why this is usually the fastest general node js count lines large file approach:
- it counts
\nbytes directly - it does not allocate one string per line
- it avoids
readlineinterface overhead
This is the right answer when you care about throughput more than elegance.
The edge case you still need to handle
If the final line does not end with \n, the last logical line was never counted as a newline byte.
That is why the code checks:
- did we read any bytes at all?
- was the last byte a newline?
Without that correction, the raw byte scanner would under-count the same file shape that readline handles correctly by default.
stream/promises pipeline
If you prefer pipeline-shaped code, Node.js also gives you stream/promises:
import { createReadStream } from 'node:fs';
import { Transform } from 'node:stream';
import { pipeline } from 'node:stream/promises';
async function countLinesPipeline(filePath) {
let count = 0;
let sawAnyByte = false;
let lastByte = 0x0a;
const counter = new Transform({
transform(chunk, encoding, callback) {
sawAnyByte = true;
for (let i = 0; i < chunk.length; i++) {
if (chunk[i] === 0x0a) {
count++;
}
}
lastByte = chunk[chunk.length - 1];
callback(null, chunk);
},
});
await pipeline(createReadStream(filePath), counter);
if (!sawAnyByte) return 0;
return lastByte === 0x0a ? count : count + 1;
}
The stream/promises API was added in Node.js v15.0.0.
This is still fundamentally the same node js createreadstream count lines idea, just packaged around pipeline.
Part 5: Trailing Newlines, Empty Files, and CRLF
This is the edge-case map that matters for node js count lines.
Naive split
For these file contents:
"a\nb"
"a\nb\n"
""
split('\n').length yields:
231
That is why node js split newline count goes wrong so easily.
readline
For the same logical inputs, local verification on Node.js 22.22.1 gave:
"a\nb"->2"a\nb\n"->2""->0
That is exactly the behavior you want in most node js readline count lines code.
Raw byte scanning
For raw stream scanning:
- each
\nincrements the counter - a final non-newline byte means one extra logical line
- an empty file must stay
0
That is why the manual scanner needs explicit final-byte logic while readline does not.
CRLF summary
For file reading, crlfDelay: Infinity is the most conservative choice:
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
The Node docs explicitly call this out as the file-reading setting that recognizes \r\n as one line break.
Part 6: A Production-Ready Node.js Line Counter
If you want one reusable node js count lines in file helper for real code, this is a practical baseline:
import { createReadStream, statSync } from 'node:fs';
import { createInterface } from 'node:readline';
export async function countLines(filePath, options = {}) {
const { countEmpty = true } = options;
const stat = statSync(filePath);
if (!stat.isFile()) {
throw new TypeError(`Not a regular file: ${filePath}`);
}
if (stat.size === 0) {
return 0;
}
const rl = createInterface({
input: createReadStream(filePath),
crlfDelay: Infinity,
});
let count = 0;
for await (const line of rl) {
if (countEmpty || line.trim() !== '') {
count++;
}
}
return count;
}
This keeps the default node js count lines async path readable while still handling:
- empty files
- CRLF input
- optional blank-line skipping
If you want to go lower level for peak throughput, swap the readline loop for the raw createReadStream scanner from the previous section.
Compatibility Notes
These are the version boundaries that actually matter:
readline'line'event: ancient baseline, available everywhere that mattersrl[Symbol.asyncIterator](): added in Node.jsv11.4.0andv10.16.0readline/promises.createInterface(): added in Node.jsv17.0.0stream/promisesandpipeline(): added in Node.jsv15.0.0rl[Symbol.dispose](): added in Node.jsv23.10.0andv22.15.0
One correction is worth making explicit: rl[Symbol.dispose]() is not a Node.js 26-only feature.
On this machine, running Node.js 22.22.1, the method already exists:
Symbol.dispose in rl; // true
But do not confuse that with safe baseline support for using syntax in all deployed Node.js environments. In current LTS code, explicit rl.close() is still the clearer assumption.
Local Benchmark Notes
I measured the main methods locally on:
- Node.js
22.22.1 - Linux
- SSD
- a hot-cache text file of about
105 MB 5,000,000fixed-width lines
Results on this machine:
readFileSync + split-> about1.52sreadlinewith'line'event -> about1.43sreadlinewithfor await...of-> about2.13sreadline/promises-> about2.09s- raw
createReadStreambyte scan -> about0.57s
These are local numbers, not universal laws. Different file systems, antivirus, cache state, line length, and CPU behavior will move the totals.
Still, the stable ranking is useful:
- raw byte scan is the fastest general answer
readlineevents beatfor await...ofhere, which matches the docs- full-file reads are simple, but they stop scaling first
Sources Checked
- Node.js
readlinedocs for'line'event behavior when input ends without a final end-of-line marker: https://nodejs.org/api/readline.html#event-line - Node.js
readlinedocs forrl[Symbol.asyncIterator]()and the note that performance is not on par with the traditional'line'event API: https://nodejs.org/api/readline.html#rlsymbolasynciterator - Node.js
readlineexample showingcrlfDelay: Infinityfor file reading: https://nodejs.org/api/readline.html#example-read-file-stream-line-by-line - Node.js
readlinePromises API docs showingreadlinePromises.createInterface()was added inv17.0.0: https://nodejs.org/api/readline.html#promises-api - Node.js
readlinedocs showingrl[Symbol.dispose]()was added inv23.10.0andv22.15.0: https://nodejs.org/api/readline.html#rlsymboldispose - Node.js
fsdocs stating callback-basedfsAPIs are preferable when maximal performance matters: https://nodejs.org/api/fs.html - Node.js
streamdocs forreadable[Symbol.asyncIterator]()and the stream Promises API: https://nodejs.org/api/stream.html
I also verified the edge cases locally on Node.js 22.22.1:
split('\n').lengthover-counts"a\nb\n"and""readlinecounts"a\nb"and"a\nb\n"as2readline/promises.createInterface()is availablerl[Symbol.asyncIterator]()andreadable[Symbol.asyncIterator]()both existrl[Symbol.dispose]()exists, butusingsyntax is not a safe assumption for this Node.js baseline
Related Guides and Tools
- TypeScript file reading
- JavaScript split newline patterns
- Python readline streaming
- Bash
wc -lcommand - Go
bufio.Scannerpatterns - Line Counter tool
Need to count lines without Node.js streaming boilerplate?
Try the Line Counter. No readline setup, no trailing-newline surprises, just the number.
Frequently Asked Questions
How do I count lines in a file with Node.js?
For small files, read the file as utf8 text and count carefully. For large files, use readline with createReadStream or scan newline bytes directly.
What is the best Node.js readline count lines pattern?
Use createInterface with createReadStream and crlfDelay Infinity. Choose for await...of for readability or the line event for a somewhat faster path.
Why does split('\n').length give the wrong count?
Because a trailing newline produces an extra empty string after the final separator, and an empty file splits into one empty string instead of zero logical lines.
Does readline count the last line without a trailing newline?
Yes. Node.js documents that the line event is also emitted when the input ends without a final end-of-line marker.
Should I use callback APIs or Promise APIs in Node.js?
Use Promises for normal application code. When performance is the priority and you have measured the bottleneck, the Node.js fs docs still prefer callback APIs.
What is the fastest Node.js count lines large file approach?
A createReadStream byte scan is usually the fastest general Node.js approach because it counts newline bytes directly without allocating one string per line.
Related Guides
11 min read
How to Count Lines in a File in TypeScript (And the readFileSync Buffer Type Trap)
Count lines in a file in TypeScript with readFileSync, readline streams, and Buffer scanning. Covers the Buffer-versus-string type trap, readline behavior when the last line has no newline, and when callback-style Node.js APIs are still worth preferring.
18 min read
How to Count Lines in JavaScript: 6 Methods with Performance Benchmarks
Count lines in JavaScript strings, files, Node.js streams, and the browser. Includes real performance benchmarks, edge case handling, and a decision guide for every scenario.
20 min read
How to Count Lines in Python: 7 Methods, Benchmarked and Battle-Tested
Count lines in Python strings, text files, large files, and directories. Includes real performance benchmarks, empty file handling, splitlines vs split, and production-ready functions.
16 min read
How to Count Lines in Bash: The Complete Guide with Edge Cases
Master line counting in Bash: count lines in files, variables, command output, and directories. Covers wc -l pitfalls, empty files, filenames with spaces, and shell script usage.