Node.js File System
Node's built-in fs module gives you full control over files—reading, writing, appending, and deleting—through synchronous, callback-based, and promise-based APIs.
The fs Module
Node.js ships with a core module called fs (short for file system) that lets you interact with files and directories without installing anything from npm. You bring it into a script with require('node:fs') or, in ESM files, import fs from 'node:fs'. Everything you'd expect from a file system API lives here: reading a file's contents, creating a new file, appending a log line, renaming a directory, or deleting a temporary file.
Synchronous vs Asynchronous Operations
Almost every fs function comes in three forms. A synchronous version, whose name ends in Sync, runs to completion and blocks the entire process until the disk operation finishes. A callback-based asynchronous version takes a function as its last argument and runs it once the operation completes, without blocking anything else. A promise-based version, available from fs/promises, returns a Promise so it pairs naturally with async/await. Picking the right one depends on where the code runs: a one-off CLI script that reads a config file at startup can safely use the Sync methods, but a running server should never use them, since blocking the event loop means every other request has to wait.
- readFileSync / writeFileSync / appendFileSync — simplest to reason about, but freeze the event loop until they finish
- fs.readFile / fs.writeFile / fs.appendFile — classic Node error-first callback style, fully non-blocking
- fs.promises.readFile (or import from 'node:fs/promises') — async/await friendly, fully non-blocking
- fs.appendFile — adds content to the end of a file without overwriting what's already there
- fs.unlink — deletes a file from disk; fs.rm can delete files or, with { recursive: true }, whole directories
Synchronous read, write, and append
const fs = require('node:fs');
// Blocks execution until the file is fully saved
fs.writeFileSync('greeting.txt', 'Hello, Node.js!\n');
// Blocks execution until the file is fully loaded
const contents = fs.readFileSync('greeting.txt', 'utf8');
console.log(contents); // Hello, Node.js!
// Append a second line without overwriting the first
fs.appendFileSync('greeting.txt', 'Second line added.\n');
console.log(fs.readFileSync('greeting.txt', 'utf8'));Callback-Based I/O and the Promises API
The callback style was Node's original approach to non-blocking file access, and it follows a strict convention: every callback receives an error object first, so if the operation succeeded that first argument is simply null. This convention is called an 'error-first callback', and you'll see it throughout older Node code and libraries. Since Node 10, the same operations are also available through fs.promises (or the standalone node:fs/promises module), where each method returns a Promise instead—letting you write flat, readable code with async/await and catch failures with an ordinary try/catch block.
Callback-based read, append, and delete
const fs = require('node:fs');
fs.writeFile('scratch.txt', 'draft entry\n', (err) => {
if (err) throw err;
fs.appendFile('scratch.txt', 'second entry\n', (err) => {
if (err) throw err;
fs.readFile('scratch.txt', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
fs.unlink('scratch.txt', (err) => {
if (err) throw err;
console.log('scratch.txt deleted');
});
});
});
});Promise-based file operations with async/await
const fs = require('node:fs/promises');
async function runLog() {
try {
await fs.writeFile('app.log', 'server started\n');
await fs.appendFile('app.log', 'listening on port 3000\n');
const log = await fs.readFile('app.log', 'utf8');
console.log(log);
await fs.unlink('app.log');
console.log('log file removed');
} catch (err) {
console.error('File operation failed:', err.message);
}
}
runLog();Exercise: Node.js File System
By default, is fs.readFile() synchronous or asynchronous?