
Node.js readFile and readFileSync: Read Files in Node
To read a file in Node.js, call await readFile('notes.txt', 'utf8') from node:fs/promises. It returns the whole file as a string. Use readFileSync from node:fs only in startup code or CLI scripts, where blocking doesn't matter. For large files, or to read a file line by line, use a stream with readline instead, because readFile loads the entire file into memory.
Reading a file in Node.js comes down to a small family of functions in the built-in fs module: fs.readFile, fs.readFileSync and readFile from fs/promises. This guide covers each one, plus the parts that trip people up: encodings and Buffers, paths, JSON, stdin, streams, errors, size limits, cancellation, TypeScript, and the Bun and Deno equivalents.
The examples below were run on Node.js 26 and use only APIs that also exist in Node 24 (as of November 2026; one newer option is marked where it appears). All references point to the official Node.js fs documentation.
The three ways to call readFile
| API | Import | Blocks the event loop? | Errors arrive as | Use it for |
|---|---|---|---|---|
readFile (promise) |
node:fs/promises |
No | rejected promise (try/catch) |
Most code |
readFile (callback) |
node:fs |
No | first callback argument err |
Older code bases |
readFileSync |
node:fs |
Yes | thrown exception | Startup config, CLI scripts |
All three take the same arguments: a path (string, Buffer, file: URL, or file descriptor) and an optional encoding string or options object { encoding, flag }. The async versions also accept signal. Since Node 24.19.0 and 26.4.0, all three also accept a buffer option for reading into memory you allocated yourself (covered below).
require('node:fs').promises and require('node:fs/promises') are the same object, so pick whichever import style your project already uses.
Every way to read a file in Node, compared
readFile is not the only option. This table covers every built-in way to get file contents, so you can pick by what you need rather than by habit:
| Method | Module | Returns | Memory use | Best for |
|---|---|---|---|---|
readFile(path, 'utf8') |
node:fs/promises |
Promise<string> |
Whole file | Config, templates, small and medium files |
readFile(path, 'utf8', cb) |
node:fs |
callback (err, data) |
Whole file | Callback-style code |
readFileSync(path, 'utf8') |
node:fs |
string |
Whole file | CLI tools, build scripts, startup |
createReadStream(path) |
node:fs |
ReadStream (chunks) |
One chunk (64 KiB default) | Large files, piping, uploads |
readline + createReadStream |
node:readline |
async iterator of lines | One chunk plus one line | Logs, CSV, line-based processing |
filehandle.readLines() |
node:fs/promises |
async iterator of lines | One chunk plus one line | Same, with less code |
filehandle.read() / fs.read() |
both | bytes read into your buffer | What you allocate | Reading a byte range, binary headers |
openAsBlob(path) |
node:fs |
Promise<Blob> |
Lazy | Passing a file to fetch or Web APIs |
import x from './x.json' with { type: 'json' } |
ES modules | parsed object | Whole file, cached | Static JSON shipped with your code |
Node.js readFile with async/await (recommended)
// read.mjs
import { readFile } from 'node:fs/promises';
try {
const text = await readFile('notes.txt', 'utf8');
console.log(text);
} catch (err) {
console.error(`Could not read file: ${err.message}`);
}
Run it with node read.mjs. Top-level await works in ES modules (.mjs files, or .js with "type": "module" in package.json).
In CommonJS, wrap the call in an async function:
// read.cjs
const { readFile } = require('node:fs/promises');
async function main() {
const text = await readFile('notes.txt', 'utf8');
console.log(text);
}
main().catch((err) => console.error(err.message));
This is the readFile example most code should copy, and the standard way to use readFile in Node.js. It doesn't block the event loop, errors flow into a normal try/catch, and it composes with Promise.all when you need several files at once.
Always pass an encoding if you want text
Without an encoding, you get a Buffer (raw bytes), not a string:
const buf = await readFile('notes.txt');
console.log(buf); // <Buffer 68 65 6c 6c 6f 0a 77 6f 72 6c 64 0a>
console.log(buf.toString()); // 'hello\nworld\n' (decodes as UTF-8)
'utf8' and { encoding: 'utf8' } are equivalent. Leave the encoding off on purpose when reading binary files such as images, PDFs or zip files, and work with the Buffer.
The fs docs are explicit about it: "If no encoding is specified (using options.encoding), the data is returned as a Buffer object. Otherwise, the data will be a string." The same rule applies to the callback version and to readFileSync, so if you are looking for where the docs say fs.readFileSync without an encoding returns a Buffer, that is the sentence (repeated under fs.readFileSync).
Read multiple files in parallel
Because the promise API returns promises, you can start several reads at once and wait for all of them:
import { readdir, readFile } from 'node:fs/promises';
import { join } from 'node:path';
const dir = './posts';
const names = (await readdir(dir)).filter((name) => name.endsWith('.md'));
const contents = await Promise.all(
names.map((name) => readFile(join(dir, name), 'utf8')),
);
console.log(`Read ${contents.length} files`);
This is much faster than awaiting each file in a loop, but it opens every file at the same time. With thousands of files you can hit EMFILE (too many open files). The Node.js errors docs note this is common on macOS, where the per-process file descriptor limit is low, and suggest raising it with ulimit -n 2048. A cleaner fix is to read in batches:
async function readAll(paths, batchSize = 100) {
const results = [];
for (let i = 0; i < paths.length; i += batchSize) {
const batch = paths.slice(i, i + batchSize);
results.push(...(await Promise.all(batch.map((p) => readFile(p, 'utf8')))));
}
return results;
}
fs.readFile with a callback
This is the original API, still fully supported:
const fs = require('node:fs');
fs.readFile('notes.txt', 'utf8', (err, data) => {
if (err) {
console.error(err.message);
return;
}
console.log(data);
});
console.log('This line prints first');
The callback runs later, so code after fs.readFile runs before the file contents arrive. That surprises most beginners. Since Node 10 the callback is mandatory; leaving it out throws a TypeError (ERR_INVALID_ARG_TYPE, per the fs.readFile history).
On success the callback always receives null as err, so if (err) is a safe check. The most common callback bug is forgetting the return after handling the error, which lets the code below run with data set to undefined.
If you are stuck with a callback API elsewhere in your code, you don't need to convert it by hand for fs: just switch the import to node:fs/promises. For other callback functions, util.promisify does the wrapping.
readFileSync: synchronous file reading
const fs = require('node:fs');
const text = fs.readFileSync('notes.txt', 'utf8');
console.log(text);
In an ES module, the same fs read file sync call looks like this:
import { readFileSync } from 'node:fs';
const text = readFileSync('notes.txt', 'utf8');
readFileSync returns the contents directly and throws on failure, so wrap it in try/catch if the file may be missing:
let config = {};
try {
config = JSON.parse(fs.readFileSync('config.json', 'utf8'));
} catch (err) {
if (err.code !== 'ENOENT') throw err; // ignore "file not found" only
}
When is readFileSync fine? At program startup, in build scripts, and in CLI tools, where nothing else is waiting. When is it wrong? Inside a web server's request handler. While the file is being read, the server cannot answer any other request.
readFile vs readFileSync: what actually differs
Both read the entire file into memory and take the same arguments. The difference is what your program can do while the read happens:
readFile (async) |
readFileSync |
|
|---|---|---|
| Returns | Promise (or calls a callback) |
the data itself |
| Event loop during the read | keeps running | blocked |
| How it reads | in chunks, yielding between them | in one go |
| Error handling | try/catch around await, or err in the callback |
try/catch |
| Good for | servers, anything concurrent | CLIs, build steps, one-time startup |
The fs docs explain the trade-off: async readFile reads "one chunk at a time, allowing the event loop to turn between each chunk", which keeps a busy process responsive but "means that it will take longer to read a complete file into memory". For a one-off script, Node readFileSync is simpler and often a bit faster. For a server, use the async version even when it is slightly slower, because blocking the event loop delays every other request.
Read from stdin with readFileSync(0, 'utf8')
The path argument can be a file descriptor, and file descriptor 0 is standard input. That gives you a one-line way to read everything piped into a script. This Node.js fs.readFileSync(0, 'utf8') stdin pattern is common in small CLI filters:
// upper.js
const fs = require('node:fs');
const input = fs.readFileSync(0, 'utf8');
process.stdout.write(input.toUpperCase());
echo "piped input" | node upper.js # PIPED INPUT
cat data.json | node -e "console.log(JSON.parse(require('fs').readFileSync(0, 'utf8')))"
Three rules from the file descriptor notes in the fs docs apply: the descriptor must support reading, Node does not close it for you, and reading starts at the descriptor's current position. If nothing is piped in and you run the script in a terminal, readFileSync(0) waits until you type and press Ctrl+D (end of input). For interactive input, use readline with process.stdin instead.
Read a file relative to your script, not your terminal
A relative path like 'notes.txt' is resolved against process.cwd(), the directory you ran node from, not the folder the script lives in. Run node src/read.mjs from the project root, and Node looks for ./notes.txt in the root, not in src/. The fs docs on string paths state this rule directly.
To read a file next to the script:
// ES modules
import { readFile } from 'node:fs/promises';
const text = await readFile(new URL('./notes.txt', import.meta.url), 'utf8');
// CommonJS
const path = require('node:path');
const text = require('node:fs').readFileSync(path.join(__dirname, 'notes.txt'), 'utf8');
In ES modules on current Node versions, import.meta.dirname is also available as the equivalent of __dirname. It was added in Node 20.11.0 and 21.2.0 and marked stable in Node 24.0.0, according to the ES modules docs:
import { join } from 'node:path';
import { readFile } from 'node:fs/promises';
const text = await readFile(join(import.meta.dirname, 'notes.txt'), 'utf8');
| You have | Directory of the current file | Full path of the current file |
|---|---|---|
CommonJS (.cjs, or .js without "type": "module") |
__dirname |
__filename |
| ES module, Node 20.11+ | import.meta.dirname |
import.meta.filename |
| ES module, any version | new URL('.', import.meta.url) |
import.meta.url (a file: URL) |
Read a JSON file
import { readFile } from 'node:fs/promises';
const config = JSON.parse(await readFile('config.json', 'utf8'));
console.log(config.port); // 3000
JSON.parse throws a SyntaxError on invalid JSON, so a single try/catch around both calls covers missing files and broken files. In ES modules you can also write import config from './config.json' with { type: 'json' };, but that file is loaded once and cached, so use readFile when the file can change while the program runs.
JSON.parse with a Buffer (no encoding)
A common line in scripts is const config = JSON.parse(fs.readFileSync('config.json')), with no encoding. It works. JSON.parse Buffer input is fine because JSON.parse converts its argument to a string first, and a Buffer converts to a UTF-8 string. Still pass 'utf8' anyway. It states the intent, TypeScript expects a string there, and it avoids decoding the same bytes twice in your head when you read the code later.
Import attributes: import JSON with { type: 'json' }
JSON modules are no longer experimental as of Node 23.1.0, 22.12.0, 20.18.3 and 18.20.5 (ES modules docs). The with { type: 'json' } part is mandatory:
import pkg from './package.json' with { type: 'json' };
console.log(pkg.version);
// dynamic form
const { default: data } = await import('./data.json', { with: { type: 'json' } });
Things to know before you switch to it:
- The import only has a default export.
import { version } from './package.json' with { type: 'json' }fails. - It is cached like any module. Edit the file while the program runs and you still see the old data.
- The path is resolved relative to the importing file, not
process.cwd(), which is often what you wanted anyway. - In CommonJS,
require('./config.json')does the same job and is also cached. - Node also has text modules (
with { type: 'text' }) that import a file as a string, but as of Node 24 and 26 they are behind the--experimental-import-textflag.
If a JSON file won't parse and you can't see why, paste it into the JSON formatter to find the broken line. For more on structuring JSON for APIs, see our JSON REST API examples.
Encodings, Buffers and binary files
Every read returns bytes. The encoding decides whether Node turns those bytes into a string, and how. These are the encodings the Buffer docs list:
| Encoding | Aliases | Use it for |
|---|---|---|
'utf8' |
'utf-8' |
Almost all text: source code, JSON, CSV, Markdown, HTML |
'utf16le' |
'utf-16le', 'ucs2', 'ucs-2' |
Files saved as UTF-16 (some Windows exports) |
'latin1' |
'binary' |
Legacy single-byte text (ISO-8859-1) |
'ascii' |
7-bit ASCII only | |
'base64' / 'base64url' |
Embedding a file in JSON, a data URL or an email | |
'hex' |
Printing bytes, checksums, debugging |
Encoding names are case-insensitive ('UTF8' works). An unknown name such as 'utf-9' throws ERR_INVALID_ARG_VALUE.
import { readFile } from 'node:fs/promises';
const png = await readFile('logo.png'); // Buffer, keep it binary
const b64 = await readFile('logo.png', 'base64');
const dataUrl = `data:image/png;base64,${b64}`;
const hex = (await readFile('notes.txt')).toString('hex');
console.log(hex); // 68656c6c6f0a776f726c640a
To check what a base64 string decodes to, or to encode a small file by hand, the Base64 encoder/decoder runs in the browser.
Why fs.readFileSync without encoding returns a Buffer
Node.js fs.readFileSync without encoding returns Buffer data for a simple reason. Node can't know whether a file is text, so by default it gives you exactly what is on disk: bytes. Decoding them as UTF-8 would corrupt images, archives and any text in another encoding. The default is also cheaper, since no string is created. When you want text, pass the encoding, or call buf.toString('utf8') later (UTF-8 is the default for toString).
Node has no built-in encoding detection. If a file might be UTF-16 or have a BOM, check the first bytes yourself: EF BB BF is a UTF-8 BOM and FF FE is a UTF-16 LE BOM.
Read only part of a file
To read just the first bytes (a file header, a magic number), open the file and read into your own buffer instead of loading everything:
import { open } from 'node:fs/promises';
const file = await open('photo.jpg');
try {
const { buffer, bytesRead } = await file.read(Buffer.alloc(4), 0, 4, 0);
console.log(bytesRead, buffer.toString('hex')); // 4 'ffd8ffe0' for many JPEGs
} finally {
await file.close();
}
Always close a FileHandle you opened, ideally in finally. The docs warn that leaked descriptors eventually crash the application.
Reuse memory with the buffer option (Node 24.19+)
Node 24.19.0 and 26.4.0 added a buffer option to readFile, fsPromises.readFile and readFileSync (fs docs). Instead of allocating a new Buffer for every read, Node reads into one you provide and returns a view of the bytes read:
import { readFile } from 'node:fs/promises';
const scratch = Buffer.alloc(64 * 1024);
const data = await readFile('small.bin', { buffer: scratch });
// data is a view over scratch, data.length === file size
// or let Node tell you the size first
const exact = await readFile('small.bin', { buffer: (size) => Buffer.alloc(size) });
If the buffer is smaller than the file, the read fails (ERR_INVALID_ARG_VALUE on Node 26). Use this in hot loops that read many small files; for everyday code the default is fine. Don't use it on Node 22 or older 24.x releases, where the option does not exist.
Node.js read file line by line
readFile loads the whole file at once. For logs, CSVs or anything large, read it line by line with a stream. This is the pattern from the readline docs:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
const rl = createInterface({
input: createReadStream('access.log'),
crlfDelay: Infinity, // treat \r\n as a single line break
});
let count = 0;
for await (const line of rl) {
if (line.includes(' 500 ')) count++;
}
console.log(`${count} server errors`);
Memory use stays small no matter how big the file is. A shorter alternative is filehandle.readLines():
import { open } from 'node:fs/promises';
const file = await open('access.log');
for await (const line of file.readLines()) {
console.log(line);
}
filehandle.readLines() was added in Node 18.11.0. It closes the file for you when the loop ends (autoClose defaults to true) and handles \r\n line endings, so on Windows-made files you get clean lines without a trailing \r.
The readline docs mention that for await...of "can be a bit slower" than listening for 'line' events. If you are processing gigabytes and need every bit of speed, use the event form:
const rl = createInterface({ input: createReadStream('huge.csv'), crlfDelay: Infinity });
rl.on('line', (line) => { /* handle line */ });
rl.on('close', () => console.log('done'));
Read a file into an array of lines
If the file is small, the simplest nodejs read file to array approach is readFile plus split:
const lines = (await readFile('list.txt', 'utf8')).split(/\r?\n/);
The regex handles both Unix (\n) and Windows (\r\n) line endings. A file that ends with a newline gives an empty last element, so add .filter(Boolean) if you don't want blank lines. For anything that might grow past a few hundred megabytes, use the streaming version above, and for CSV data with quoted commas, use a real CSV parser rather than split(','). (To look at a CSV quickly, the CSV to JSON converter shows how it parses.)
Read large files with createReadStream (streams)
A stream hands you the file in chunks as it reads them, so memory use depends on the chunk size, not the file size. fs.createReadStream reads 64 KiB at a time by default (highWaterMark):
import { createReadStream } from 'node:fs';
let bytes = 0;
for await (const chunk of createReadStream('video.mp4')) {
bytes += chunk.length; // chunk is a Buffer
}
console.log(`${bytes} bytes`);
Useful options, from the createReadStream docs:
| Option | Default | What it does |
|---|---|---|
encoding |
null (Buffers) |
Emit strings instead of Buffers |
highWaterMark |
64 * 1024 |
Chunk size in bytes |
start, end |
0, Infinity |
Read a byte range; both are inclusive and 0-based |
signal |
An AbortSignal that destroys the stream |
|
autoClose |
true |
Close the file when done or on error |
// first 5 bytes only
const chunks = [];
for await (const c of createReadStream('notes.txt', { start: 0, end: 4 })) chunks.push(c);
console.log(Buffer.concat(chunks).toString()); // 'hello'
To send a file somewhere (to an HTTP response, a compressor, another file), use pipeline, which also cleans up on errors:
import { createReadStream, createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';
import { createGunzip } from 'node:zlib';
await pipeline(
createReadStream('backup.json.gz'),
createGunzip(),
createWriteStream('backup.json'),
);
Async readFile on regular files, readFile on pipes or files of unknown size, and the default highWaterMark of createReadStream and filehandle.readLines. Source: Node.js fs docs
Cancel a read with AbortSignal
Since Node 14.17.0 and 15.2.0, readFile accepts an AbortSignal. An aborted read rejects with an AbortError (err.name === 'AbortError', err.code === 'ABORT_ERR'):
import { readFile } from 'node:fs/promises';
try {
const text = await readFile('slow-network-share.txt', {
encoding: 'utf8',
signal: AbortSignal.timeout(5000), // give up after 5 seconds
});
console.log(text.length);
} catch (err) {
if (err.name === 'AbortError') {
// err.cause.name is 'TimeoutError' when AbortSignal.timeout fired
console.error('Read was cancelled or took too long');
} else {
throw err;
}
}
Use an AbortController when the cancel comes from your own code (a user hits cancel, a request closes). One limit from the docs: aborting "does not abort individual operating system requests but rather the internal buffering fs.readFile performs". A read already sent to the OS still finishes; Node just stops collecting the result. readFileSync takes no signal, since nothing else can run while it blocks.
Common errors and how to fix them
| Error code | Meaning | Fix |
|---|---|---|
ENOENT |
No such file | Check the path, and remember it is relative to process.cwd() |
EISDIR |
The path is a directory | Use readdir for directories |
EACCES / EPERM |
No permission | Check file permissions or run as the right user |
EMFILE |
Too many open files | Read fewer files at once (batch them), or raise the limit with ulimit -n |
ERR_FS_FILE_TOO_LARGE |
File is over 2 GiB | Use createReadStream; readFile has a 2 GiB limit |
ERR_STRING_TOO_LONG |
Decoded text exceeds V8's max string length | Read as a Buffer or stream it; don't decode the whole file to one string |
ERR_INVALID_ARG_TYPE |
Missing callback, or a non-path argument | Pass a callback to fs.readFile, or use node:fs/promises |
ERR_INVALID_ARG_VALUE |
Unknown encoding, or buffer option too small |
Use a listed encoding, allocate a larger buffer |
AbortError |
You aborted via signal |
Expected; handle it separately from real failures |
Check err.code rather than parsing err.message. The code is stable across versions and platforms.
System errors also carry err.syscall and err.path. A missing file gives code: 'ENOENT', syscall: 'open', path: 'nope.txt', which tells you exactly which path Node tried.
On directories, the docs note the behavior is platform-specific: macOS, Linux and Windows return EISDIR, while FreeBSD returns a representation of the directory's contents. Don't rely on either; check with stat first if you accept user-supplied paths that might be folders.
Should you check if the file exists first?
Usually not. The fs docs warn that checking with access() before opening "introduces a race condition, since other processes may change the file's state between the two calls". Read the file directly and handle ENOENT:
async function readIfExists(path) {
try {
return await readFile(path, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') return null;
throw err;
}
}
fs.readFileSync is not a function
You'll see this TypeError for one of two reasons:
- You imported the promise module.
node:fs/promiseshasreadFilebut noreadFileSync. ImportreadFileSyncfromnode:fs. - The code runs in the browser. Bundlers replace
fswith an empty object in client code. In Next.js or Vite, move the read into server-only code (a server component, an API route,getStaticProps, a build script).
Performance and limitations
readFilebuffers the entire file. The docs say to preferfs.createReadStream()"to minimize memory costs" (fs.readFile).- Async
readFilereads in chunks (512 KiB per read for regular files), letting the event loop run between chunks. That makes it friendlier to a busy server, but slightly slower than one big read. For "as-fast-as-possible" reading, the docs recommend callingfs.read()directly and managing the buffer yourself. - The promise API isn't always fastest. The fs docs note that the promise APIs run on the libuv thread pool and recommend the callback APIs when maximum performance is needed. For most apps the difference doesn't matter; pick the promise API for readability.
- Aborting with
AbortSignalstops Node's internal buffering, but not an operating-system read already in progress. - Reading from a file descriptor starts at the descriptor's current position, and Node does not close it for you.
How big a file can readFile handle?
There are three separate ceilings, and the lowest one wins:
| Limit | Value | Where it comes from |
|---|---|---|
readFile / readFileSync file size |
2 GiB | ERR_FS_FILE_TOO_LARGE, "an internal I/O constraint", not a Buffer limit |
Single Buffer |
2^53 - 1 bytes on 64-bit, 2^31 - 1 on 32-bit (since Node 22) | buffer.constants.MAX_LENGTH |
| Single string | 536,870,888 UTF-16 code units on Node 26 (64-bit) | buffer.constants.MAX_STRING_LENGTH, engine-dependent |
The string limit is the one people hit first. A 600 MiB ASCII file reads fine as a Buffer, but readFileSync(path, 'utf8') on it throws ERR_STRING_TOO_LONG ("Cannot create a string longer than 0x1fffffe8 characters") on Node 26. So in practice: text files up to a few hundred megabytes can go through readFile; above that, stream them. Check your own runtime's numbers with:
node -p "require('node:buffer').constants"
Memory is the softer limit. A 300 MiB file read with readFile needs 300 MiB for the Buffer and, if you decode it, more for the string, all at once. With a stream, peak memory stays near one chunk.
Node.js read file version history
- v0.1.x
fs.readFileSync(v0.1.8),fs.readFile(v0.1.29) andfs.createReadStream(v0.1.31) are added. - v5.0.0The path argument can be a file descriptor, which is what makes
readFileSync(0, 'utf8')for stdin work. - v7.6.0Paths can be WHATWG
URLobjects with thefile:protocol, sonew URL('./x', import.meta.url)works. - v10.0.0
fsPromises.readFileis added, and thefs.readFilecallback becomes mandatory (aTypeErrorwithout it). - v14.17.0 / v15.2.0
readFileaccepts anAbortSignal. - v18.11.0
filehandle.readLines()is added. - v19.8.0
fs.openAsBlob()is added (stable in v24.0.0). - v20.11.0 / v21.2.0
import.meta.dirnameandimport.meta.filenameare added (stable in v24.0.0). - v22.0.0
buffer.constants.MAX_LENGTHbecomes 2^53 - 1 on 64-bit systems. - v22.12.0 / v23.1.0JSON modules (
with { type: 'json' }) are no longer experimental. - v24.19.0 / v26.4.0
readFile,fsPromises.readFileandreadFileSyncaccept abufferoption. - Oct 28, 2026Node 26 (Lithium) becomes LTS with v26.13.0, alongside Node 24 (Krypton) and Node 22 (Jod).
Versions come from the "History" tables in the fs, buffer and ES modules docs, and release dates from the Node.js release index.
TypeScript: read file with types
The same calls work in TypeScript, and the types follow the encoding argument: with an encoding you get string, without one you get a Buffer.
// read.ts
import { readFile } from 'node:fs/promises';
const text: string = await readFile('notes.txt', 'utf8');
const bytes = await readFile('logo.png'); // Buffer
type Config = { port: number };
const config = JSON.parse(await readFile('config.json', 'utf8')) as Config;
Install the Node type definitions with npm i -D @types/node. JSON.parse returns any, so the as Config cast is a promise to the compiler, not a check; validate untrusted files with a schema library if their shape matters. (The JSON to TypeScript tool generates a starting type from a sample file.)
In a project with "type": "module" (needed for the top-level await), Node 24 can run this file directly with node read.ts: type stripping is enabled by default since Node 23.6.0 and became stable in Node 24.12.0, per the Node.js TypeScript docs. It only removes types; it doesn't type-check, so keep running tsc --noEmit in CI.
Bun and Deno equivalents of readFile
Both runtimes run the node:fs code above, and both also have their own APIs:
| Task | Node.js | Bun | Deno |
|---|---|---|---|
| Read as text | await readFile(p, 'utf8') |
await Bun.file(p).text() |
await Deno.readTextFile(p) |
| Read as bytes | await readFile(p) (Buffer) |
await Bun.file(p).bytes() (Uint8Array) |
await Deno.readFile(p) (Uint8Array) |
| Read JSON | JSON.parse(await readFile(p, 'utf8')) |
await Bun.file(p).json() |
JSON.parse(await Deno.readTextFile(p)) |
| Sync text | readFileSync(p, 'utf8') |
readFileSync from node:fs |
Deno.readTextFileSync(p) |
| Stream or partial read | createReadStream(p) |
Bun.file(p).stream() |
Deno.open(p), then file.read(buf) |
| Permission needed | none | none | -R / --allow-read |
// Bun
const file = Bun.file('notes.txt'); // lazy: nothing is read yet
if (await file.exists()) {
console.log(file.size, await file.text());
}
// Deno: run with deno run -R main.ts
const text = await Deno.readTextFile('notes.txt');
console.log(text);
Sources: Bun file I/O docs (Bun.file is lazy: creating it "does not read the file from disk", and Bun describes its node:fs support as nearly complete), Deno reading files example and Deno Node.js compatibility (Node built-ins are imported with the node: prefix; -R is short for --allow-read).
If you want code that runs everywhere, stick to node:fs/promises.
Which method should you use?
try/catch error handling, works with Promise.all. The default choice for a node read file to string.\r\n.Can JavaScript read files in the browser?
Not with fs. The readFile JS function you have seen in Node examples is not part of the browser. The fs module exists only in Node.js (and in compatible runtimes such as Deno and Bun). In the browser, the user must pick a file, and you read it with the File API:
// <input type="file" id="picker"> in your HTML
document.querySelector('#picker').addEventListener('change', async (event) => {
const file = event.target.files[0];
const text = await file.text();
console.log(text);
});
Older code uses FileReader, which does the same thing with events instead of promises. To load a file from your own server, use fetch('/data.json'). To keep small amounts of data in the browser between visits, see our guide to localStorage in JavaScript.
If you are debugging a script that reads the wrong file, our Node.js debugging guide shows how to step through it. For libraries that go beyond fs (cloud storage, databases, file uploads), see top Node.js SDKs. You can find editors and CLI helpers in DevHunt's IDE tools and CLI tools categories.
FAQ
What is the difference between readFile and readFileSync?
readFile is asynchronous: it returns a promise (or calls a callback), and your program keeps running while the file loads. readFileSync blocks until the file is read and returns the contents directly. Both read the whole file into memory and accept the same path and encoding arguments.
Why does fs.readFile return a Buffer instead of a string?
Because you didn't pass an encoding. Call readFile(path, 'utf8') to get a string, or call .toString('utf8') on the Buffer.
How do I read a file synchronously in Node.js?
const text = require('node:fs').readFileSync('file.txt', 'utf8'); In an ES module, use import { readFileSync } from 'node:fs';. Wrap it in try/catch, because it throws if the file is missing.
Should I import from 'fs' or 'node:fs'?
Both work. The node: prefix makes it explicit that you mean the built-in module, not an npm package named fs, and it is the form used throughout the current Node.js docs.
How do I read a large file in Node.js?
Don't use readFile for files that are hundreds of megabytes or more, and it fails outright above 2 GiB. Use createReadStream, with readline if you need lines, to process the file in chunks.
How do I use fs promises readFile with async/await?
Import it from node:fs/promises and await it: const text = await readFile('file.txt', 'utf8'). In an ES module this works at the top level; in CommonJS, put it inside an async function. Errors become rejected promises, so wrap the await in try/catch.
How do I read stdin with fs.readFileSync(0, 'utf8')?
File descriptor 0 is standard input, so readFileSync(0, 'utf8') returns everything piped into the process as one string, for example cat data.txt | node script.js. It waits for end of input, and Node does not close the descriptor for you.
Is nodejs readFileSync bad for performance?
Not by itself. It is often slightly faster than async readFile for a single file, because it reads in one go. The problem is that it blocks the event loop, so in a server every other request waits. Use it at startup and in scripts, and use the async version in request handlers.
Can JSON.parse read a Buffer from readFileSync?
Yes. JSON.parse(fs.readFileSync('config.json')) works because the Buffer is converted to a UTF-8 string first. Passing 'utf8' is still clearer, and it is what TypeScript's types expect.
Why is fs.readFileSync not a function?
You probably imported from node:fs/promises, which has no sync methods, or the code is running in a browser bundle, where fs doesn't exist. Import readFileSync from node:fs and make sure the code only runs on the server.
How do I read a file in Node.js line by line?
Use for await (const line of (await open(path)).readLines()) from node:fs/promises, or readline.createInterface with createReadStream and crlfDelay: Infinity. Both keep memory use small regardless of file size, which is why they are the standard answer for nodejs read file line by line.
What encoding should I use with readFile?
'utf8' for almost every text file. Use no encoding for binary files (images, PDFs, archives), 'base64' when you need to embed the file as text, and 'utf16le' or 'latin1' only for files you know were saved that way.