Programmatic text comparison
Compare Two UTF-8 Text Files With Node.js
Compare two local UTF-8 text files in Node.js with strict decoding, explicit size limits and distinct exit codes for equal, different and failed comparisons.
In Node.js, read each file, decode it using the intended text encoding and compare the resulting strings. Decide first whether line endings, a byte-order mark, spaces or Unicode normalization should count as differences. The example below performs exact decoded UTF-8 text comparison and leaves both files unchanged.
Define what equal means
This program treats CRLF and LF line endings as different. It also preserves a leading UTF-8 byte-order mark as a character, so a file containing that mark differs from otherwise identical text without it. It does not trim whitespace, fold case or normalize accents.
Those decisions are deliberate. A configuration file with an extra space or a differently capitalized identifier may need an exact comparison, while a document-review workflow might choose different rules. State the rule with the result instead of silently cleaning both inputs until they match.
Save the program as compare-text.mjs
import { open } from "node:fs/promises";
async function readUtf8(path) {
const handle = await open(path, "r");
try {
if (!(await handle.stat()).isFile()) throw new Error("Use regular files");
const chunks = [];
let size = 0;
for await (const chunk of handle.createReadStream({ autoClose: false })) {
size += chunk.length;
if (size > 4 * 1024 * 1024) throw new Error("File exceeds 4 MiB");
chunks.push(chunk);
}
const bytes = Buffer.concat(chunks);
const text = new TextDecoder("utf-8", {
fatal: true, ignoreBOM: true
}).decode(bytes);
return { text, size };
} finally {
await handle.close();
}
}
try {
const paths = process.argv.slice(2);
if (paths.length !== 2) throw new Error("Supply exactly two file paths");
const a = await readUtf8(paths[0]);
const b = await readUtf8(paths[1]);
const equal = a.text === b.text;
console.log(JSON.stringify({
equal, comparison: "exact decoded UTF-8 text",
bytes: [a.size, b.size],
utf16Units: [a.text.length, b.text.length]
}, null, 2));
process.exitCode = equal ? 0 : 1;
} catch (error) {
console.error(error.message);
process.exitCode = 2;
}
Run node compare-text.mjs first.txt second.txt from a terminal, quoting a file path if it contains spaces. The program prints a JSON result and uses exit code zero for equal text, one for different text and two for an input or processing failure. A missing file is an error, not a different-text verdict.
Reject damaged or unexpected UTF-8
The fatal decoder rejects invalid UTF-8 rather than silently replacing damaged bytes with a replacement character. The ignoreBOM option is intentionally true: for this API, that preserves the mark in the decoded text instead of treating it as an encoding signature to remove.
MDN documents TextDecoder, and Node’s file-system documentation describes file handles and streams. If your files use another encoding, convert them deliberately or adapt the decoding contract; do not label an encoding error as proof that the documents differ.
Check equal and unequal examples
Two files containing alpha followed by a newline compare equal. Replacing the second file’s text with beta produces equal: false. Keeping alpha but changing LF to CRLF also produces false under this exact rule.
Two empty files compare equal. An empty file and a file containing one newline do not. Likewise, café written with a precomposed accented letter differs from cafe followed by a combining accent, even if both render similarly. Unicode normalization is an optional policy change, not something this program performs automatically.
Understand the resource and output limits
Each file is limited to four mebibytes. The reader checks accumulated bytes while streaming, then holds the accepted bytes and decoded text in memory. This bounds ordinary input size but is not a streaming diff algorithm for very large files.
The result reports byte sizes and JavaScript UTF-16 code-unit lengths with separate labels. Neither field is a word count or a count of user-perceived characters. The equality flag answers whether the decoded strings match; it does not identify changed lines.
Choose a diff when you need locations
If you need to inspect edits, use a line or word diff after establishing the appropriate normalization rule. The local Text Compare tool can support interactive review of supplied text, while this script provides a repeatable exact-file check.
Keep source files stable while comparing them. Reading a file while another process rewrites it does not create an atomic snapshot of the pair. For a reliable review, compare saved copies representing the versions you actually intend to evaluate.


