Reading and writing files is one of the most common jobs for a Node.js program. Command-line tools process CSV exports, servers write log files, build tools read source code and write bundles, and small apps keep their data in JSON files. In Node, all of this goes through the built-in fs (file system) module.
The fs module comes in three styles. The promise API, imported from node:fs/promises, works with async and await and is the modern choice for most code. The callback API is Node's original style, still found in older code. The synchronous API, with names ending in Sync, blocks the event loop until the operation finishes; it's fine for short scripts and startup code but should be avoided inside servers.
In this lesson you'll read and write text and JSON files, append to logs, create and list folders, get information about files, handle the errors that file operations produce, and learn which style to use where. File paths are built with the path module, covered in detail in the next lesson.
Reading and writing files
import { readFile, writeFile, appendFile } from 'node:fs/promises';
await writeFile('notes.txt', 'First line\n'); // create or overwrite
await appendFile('notes.txt', 'Second line\n'); // add to the end
const text = await readFile('notes.txt', 'utf8'); // read as a string- Pass an encoding such as
'utf8'to get a string; without it,readFilereturns a Buffer of raw bytes, which is right for images and other binary files. writeFilereplaces the whole file. UseappendFileto add to it.- Relative paths are resolved against the current working directory, the folder you ran
nodefrom, not the script's folder. Useimport.meta.dirnameto build paths next to your script.
Folders and file information
| Function | What it does |
|---|---|
mkdir(path, { recursive: true }) | Creates a folder and any missing parents; no error if it exists |
readdir(path) | Lists the names inside a folder |
readdir(path, { withFileTypes: true }) | Lists entries with isFile() and isDirectory() |
stat(path) | Size, creation and modification times, file or folder |
rename(from, to) | Renames or moves a file |
copyFile(from, to) | Copies a file |
rm(path, { recursive: true, force: true }) | Deletes a file or folder |
Errors and existence checks
File operations fail with an error that has a code property:
ENOENT: the file or folder doesn't exist.EEXIST: it already exists.EACCESorEPERM: permission denied.EISDIR: you tried to read a folder as a file.
Rather than checking whether a file exists and then reading it (it could disappear in between), just try the operation and handle the error:
try {
const data = await readFile('settings.json', 'utf8');
} catch (err) {
if (err.code === 'ENOENT') console.log('No settings yet, using defaults');
else throw err;
}Node.js Basics Syntax
fs/promises
import fs from 'node:fs/promises';
const text = await fs.readFile('a.txt', 'utf8');
await fs.writeFile('b.txt', 'hello');
await fs.appendFile('log.txt', 'line\n');
const data = JSON.parse(await fs.readFile('data.json', 'utf8'));
await fs.writeFile('data.json', JSON.stringify(data, null, 2));
await fs.mkdir('out/reports', { recursive: true });
const entries = await fs.readdir('out', { withFileTypes: true });
const info = await fs.stat('a.txt'); // info.size, info.mtime
await fs.rm('out', { recursive: true, force: true });
import { readFileSync } from 'node:fs'; // sync version for scriptsNode.js Basics Summary
- Import from node:fs/promises and use await with readFile, writeFile and appendFile.
- Pass 'utf8' to read text; without an encoding you get a Buffer.
- mkdir with recursive: true, readdir, stat, rename, copyFile and rm manage files and folders.
- Errors carry codes such as ENOENT and EEXIST; try the operation and handle the error.
- Avoid Sync functions inside servers because they block the event loop.
Node.js Basics Examples
Each example shows the code, the output it produces and what each part does.
A simple log file
import { writeFile, appendFile, readFile } from 'node:fs/promises';
const file = 'app.log';
await writeFile(file, '');
async function log(level, message) {
const stamp = '2026-03-01T09:00:00Z';
await appendFile(file, `${stamp} [${level}] ${message}\n`);
}
await log('INFO', 'Server started');
await log('WARN', 'Disk space low');
await log('INFO', 'User signed in');
const lines = (await readFile(file, 'utf8')).trim().split('\n');
console.log(`${lines.length} entries`);
console.log(lines.filter((l) => l.includes('[WARN]')).join('\n'));3 entries
2026-03-01T09:00:00Z [WARN] Disk space lowLine-by-line explanation
- Line 4writeFile with an empty string creates the file, or empties it if it exists.
- Lines 6 to 9Each call adds one line to the end of the file. A real logger would use new Date().toISOString().
- Line 15readFile with utf8 returns a string, split into lines.
- Line 17Ordinary array methods filter the lines.
Saving and loading JSON data
import { readFile, writeFile } from 'node:fs/promises';
const file = 'todos.json';
async function load() {
try {
return JSON.parse(await readFile(file, 'utf8'));
} catch (err) {
if (err.code === 'ENOENT') return [];
throw err;
}
}
async function save(todos) {
await writeFile(file, JSON.stringify(todos, null, 2));
}
await writeFile(file, '[]');
let todos = await load();
todos.push({ id: 1, title: 'Buy milk', done: false });
todos.push({ id: 2, title: 'Write report', done: true });
await save(todos);
todos = await load();
console.log(`${todos.length} todos, ${todos.filter((t) => !t.done).length} open`);
console.log((await readFile(file, 'utf8')).split('\n')[2]);2 todos, 1 open
"id": 1,Line-by-line explanation
- Line 7JSON.parse turns the file's text back into an array.
- Line 9A missing file isn't an error here: it just means no todos yet.
- Line 10Any other error, such as invalid permissions, is passed on.
- Line 15JSON.stringify with 2 writes indented, readable JSON.
- Line 26The third line of the saved file shows the indentation.
Folders, listing and file information
import { mkdir, writeFile, readdir, stat, rm } from 'node:fs/promises';
import { join } from 'node:path';
const root = 'reports-demo';
await rm(root, { recursive: true, force: true });
await mkdir(join(root, '2026', 'march'), { recursive: true });
await writeFile(join(root, 'summary.txt'), 'Total: 1204 orders');
await writeFile(join(root, 'notes.md'), '# Notes');
const entries = await readdir(root, { withFileTypes: true });
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
console.log(`${e.isDirectory() ? 'folder' : 'file '} ${e.name}`);
}
const info = await stat(join(root, 'summary.txt'));
console.log(`summary.txt is ${info.size} bytes, file: ${info.isFile()}`);
try {
await stat(join(root, 'missing.txt'));
} catch (err) {
console.log(`Error code: ${err.code}`);
}folder 2026
file notes.md
file summary.txt
summary.txt is 18 bytes, file: true
Error code: ENOENTLine-by-line explanation
- Line 5force: true means no error if the folder doesn't exist yet.
- Line 6recursive: true creates 2026 and march in one call.
- Line 10withFileTypes returns entries that know whether they're files or folders.
- Line 11The names are sorted, because readdir doesn't promise any order.
- Line 15stat reports the size in bytes, among other details.
- Lines 19 to 21A missing file produces the ENOENT error code.
Common Node.js Basics Mistakes
Forgetting the encoding
const text = await readFile('notes.txt');
console.log(text.toUpperCase()); // TypeError: text.toUpperCase is not a function (it's a Buffer)const text = await readFile('notes.txt', 'utf8');
console.log(text.toUpperCase());Forgetting await
const data = readFile('data.json', 'utf8');
JSON.parse(data); // data is a Promise, not textconst data = await readFile('data.json', 'utf8');
JSON.parse(data);Sync file reads inside a request handler
app.get('/report', (req, res) => {
res.send(readFileSync('big-report.html', 'utf8')); // blocks every other request
});app.get('/report', async (req, res) => {
res.send(await readFile('big-report.html', 'utf8'));
});Node.js Basics in Real Projects
File handling in Node projects
Build tools such as Vite and webpack read thousands of source files and write bundles with fs. Static site generators read Markdown and write HTML. Servers write logs and uploaded files, CLI tools process CSV and JSON exports, and configuration is often read once at startup from a JSON file. For very large files, Node's streams process data piece by piece instead of loading it all into memory.
Node.js Basics FAQ
How do I check if a file exists in Node.js?
In most cases, don't check first: try to read or open it and catch the ENOENT error, because the file could be created or deleted between a check and the real operation. When you only need a yes or no, for example in a startup script, existsSync(path) from node:fs returns true or false. The old callback fs.exists is deprecated.
How do I read a file line by line in Node.js?
Open it as a stream and pass it to readline: const rl = createInterface({ input: createReadStream('log.txt'), crlfDelay: Infinity }), then loop with for await (const line of rl). This keeps memory use low even for multi-gigabyte files. For small files, (await readFile(path, 'utf8')).split('\n') is simpler.
What is the difference between readFile and createReadStream?
readFile loads the whole file into memory and gives it to you in one piece, which is easiest for small and medium files. createReadStream delivers the file in chunks (64 KB by default) as they're read, so memory use stays flat whatever the file size. Use streams for large files, uploads and anything you're piping on to another destination.
Why does writeFile throw ENOENT when I'm creating a new file?
writeFile creates the file but not the folders above it, so the error means a folder in the path doesn't exist. Create it first with await mkdir(path.dirname(file), { recursive: true }), which does nothing if the folder is already there. Also check that the relative path is relative to where you ran node, not to the script.
How do I write to a file safely so a crash doesn't leave it half-written?
Write the new content to a temporary file in the same folder, then rename it over the original. On Linux and macOS, a rename within one file system replaces the file in a single step, so readers see either the old version or the new one, never a partial file. Many libraries that save JSON settings use exactly this pattern.
Node.js Basics Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the line to read the file as text.
const text = await readFile('poem.txt', '');
Which import gives you promise-based file functions?
What does the error code ENOENT mean?
What does this print?
await writeFile('a.txt', 'one');
await writeFile('a.txt', 'two');
console.log(await readFile('a.txt', 'utf8'));
Why avoid readFileSync in a web server's request handler?
Node.js Basics Quiz
5 questions to check what you learned.
Which function adds text to the end of a file?
- appendFile
- writeFile
- addFile
- readFile
What does mkdir with { recursive: true } do?
- Deletes folders recursively
- Lists folders
- Creates any missing parent folders and doesn't fail if the folder exists
- Copies folders
What does readFile return when no encoding is given?
- A string
- An array of lines
- null
- A Buffer of bytes
Relative paths in fs calls are resolved against which folder?
- The current working directory where node was started
- The script's folder
- The home folder
- node_modules
Which is the safer way to read a file that might not exist?
- Check with existsSync, then read
- Use writeFile first
- Use stat in a loop
- Try to read it and handle the ENOENT error