Config files, uploads, logs, generated output — sooner or later every Node program touches the file system.
The node:fs module offers three styles of the same operations: callbacks (fs.readFile(path, cb)), synchronous (fs.readFileSync) and promises. In application code use the promise API from `node:fs/promises` with await.
The everyday functions:
readFile(path, "utf8")— whole file as a string (omit the encoding to get aBufferof bytes).writeFile(path, data)— create or replace;appendFileadds to the end.readdir(dir, { withFileTypes: true })— entries withisFile()/isDirectory(); addrecursive: trueto walk subfolders.mkdir(dir, { recursive: true })— creates parents and doesn't fail if it already exists.stat(path)— size, timestamps, type.rm(path, { recursive: true, force: true })— delete files or trees.rename,copyFile,cp(recursive copy),access.
Failures reject with an error whose code tells you what happened: ENOENT (no such file), EACCES (permission), EEXIST, EISDIR. Check err.code rather than the message text.
Don't call access() or stat() to check whether a file exists before opening it — another process can change things in between. Just attempt the operation and handle ENOENT.
Synchronous functions block the event loop for the whole operation. They are fine in a CLI script or at startup (loading config once) but never inside a request handler.
readFile loads everything into memory; for large files or files you process line by line, use streams (later lesson). Relative paths resolve against the process's current working directory, not the file's location — build absolute paths with import.meta.dirname (next lesson).
Example
import { readFile, writeFile, mkdir, readdir } from "node:fs/promises";
import { join } from "node:path";
const dir = join(import.meta.dirname, "out");
await mkdir(dir, { recursive: true }); // ok if it exists
await writeFile(join(dir, "notes.txt"), "first line\n");
const text = await readFile(join(dir, "notes.txt"), "utf8");
console.log(text.trim()); // first line
for (const entry of await readdir(dir, { withFileTypes: true })) {
console.log(entry.name, entry.isFile() ? "file" : "dir");
}
async function readConfig(path) {
try {
return JSON.parse(await readFile(path, "utf8"));
} catch (err) {
if (err.code === "ENOENT") return {}; // missing file -> defaults
throw err; // anything else is a real problem
}
}
console.log(await readConfig("nope.json")); // {}Remember
- Use
node:fs/promiseswith await in application code - Handle errors by
err.code(ENOENT, EACCES...), don't pre-check existence - Sync APIs block the event loop — only for scripts and startup
Watch out for
- Using relative paths and being surprised when the script is run from another directory.
- Reading huge files with readFile instead of streaming them.