fsPromises.open()
Opens a file and resolves with a FileHandle for positional reads, writes and streaming.
fsPromises.open(path, flags[, mode])- Added in Node.js v10.0.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | Buffer | URL | File to open. |
$flagsoptional | string | number= 'r' | File system flag: 'r' read, 'r+' read/write, 'w' truncate/create for writing, 'wx' create exclusively, 'a' append, 'a+' read and append. |
$modeoptional | integer= 0o666 | Sets the file mode (permission and sticky bits), but only if the file was created. |
Return value
Promise<FileHandle>Fulfills with a FileHandle object wrapping the open file descriptor.
Errors
Rejects with ENOENT for a missing file under a read flag, EEXIST under 'wx'/'ax' when the file exists, EISDIR when the path is a directory, and EMFILE once the process runs out of descriptors — the symptom of leaked handles.
Changelog
- PHP v15.13.0, v14.17.0The flags argument is now optional and defaults to 'r'.
- PHP v10.0.0Added in v10.0.0.
fsPromises.open() gives you a FileHandle: an object wrapping one open file descriptor, with methods like read(), write(), readFile(), writeFile(), truncate(), stat(), sync(), readLines() and createReadStream().
When to reach for it
Reach for it when a whole-file helper is not enough: reading a header without loading the rest, patching bytes at an offset, appending thousands of records through one descriptor instead of thousands, or streaming a file line by line.
Close it in a finally block
A FileHandle holds a real descriptor. If you never call close(), it stays open until garbage collection — and Node emits a warning and may leak until the process hits EMFILE. Wrap the work in try { … } finally { await handle.close(); } so an exception mid-read still releases it. Node 24 added Symbol.asyncDispose support, so await using handles this where your syntax level allows it.
What trips people up
Reads and writes without an explicit position advance an internal cursor. After writing, handle.readFile() reads from the end and returns empty — pass a position or reopen.
Example
When to use it
- Read only the first bytes of a file to sniff its format.
- Patch a fixed-width field in place without rewriting the whole file.
- Append many log records through a single descriptor.
- Iterate a large text file line by line with handle.readLines().
Frequently asked questions
Do I have to close a FileHandle in Node.js?
Yes. Call await handle.close() in a finally block — an unclosed handle keeps a file descriptor alive until garbage collection, triggers a runtime warning, and eventually causes EMFILE.
When should I use fsPromises.open instead of readFile?
When you need part of a file, a specific offset, many operations on one descriptor, or a stream. readFile is simpler and better whenever you want the entire contents in one go.
Why does handle.readFile() return an empty string after I write?
Because reads and writes without an explicit position share a cursor, which your write left at the end of the file. Pass a position to handle.read(), or read the path with fsPromises.readFile() after closing.
More examples
Read a file line by line
Prints line: row 1 through line: row 3. readLines() streams, so memory stays flat whether the file is three lines or three million — the reason to open a handle instead of splitting readFile output.
import fs from 'node:fs/promises';
const writer = await fs.open('data.txt', 'w');
try {
for (let i = 1; i <= 3; i++) await writer.write(`row ${i}\n`);
await writer.sync();
} finally {
await writer.close();
}
const reader = await fs.open('data.txt', 'r');
try {
for await (const line of reader.readLines()) {
console.log('line:', line);
}
} finally {
await reader.close();
}
await fs.rm('data.txt');Sniff a file header without reading the body
Prints magic: 89504e47 and looks like png: true after touching four bytes of a 4 KB file. readFile would have pulled the whole thing into memory to answer the same question.
import fs from 'node:fs/promises';
const png = Buffer.concat([
Buffer.from([0x89, 0x50, 0x4e, 0x47]),
Buffer.alloc(4096, 0),
]);
await fs.writeFile('image.png', png);
const handle = await fs.open('image.png', 'r');
try {
const magic = Buffer.alloc(4);
await handle.read(magic, 0, 4, 0);
console.log('magic:', magic.toString('hex'));
console.log('looks like png:', magic.toString('hex') === '89504e47');
} finally {
await handle.close();
}
await fs.rm('image.png');Closing on the error path too
Prints the caught error, the closed message, then open failed: ENOENT. Note the optional chaining: if open() itself rejects, handle is still undefined, so an unguarded handle.close() in finally would replace your real error with a TypeError.
import fs from 'node:fs/promises';
await fs.writeFile('short.txt', 'hi');
let handle;
try {
handle = await fs.open('short.txt', 'r');
throw new Error('parsing blew up');
} catch (err) {
console.log('caught:', err.message);
} finally {
await handle?.close();
console.log('handle closed even though the body threw');
}
try {
await fs.open('not-here.txt');
} catch (err) {
console.log('open failed:', err.code);
}
await fs.rm('short.txt');
Discussion