Reading and Writing Files
Most useful programs need to store or retrieve information: saving user data, reading a settings file, or keeping a log. Node.js gives you this ability through the fs module — a built-in set of tools where "fs" stands for "file system," meaning the folders and files on your computer.
This chapter assumes you can run a script with node and understand basic async/await from earlier chapters.
The two ways to work with files
Every file operation can happen in two styles:
- Blocking (synchronous): your program stops and waits until the file is done. Nothing else happens in the meantime.
- Non-blocking (asynchronous): your program asks for the file, continues doing other work, and gets notified when the file is ready.
Node.js is famous for the second style, so we will use it. The fs module offers a promise-based version designed for this, which we load like this:
const { readFile, writeFile } = require('node:fs/promises');A promise is a stand-in for a result that will arrive later. The await keyword lets you write code that waits for it without freezing everything else.
Reading a file
Create a file called notes.txt with any text inside. Then run this script:
// read.js
const { readFile } = require('node:fs/promises');
async function main() {
try {
const text = await readFile('notes.txt', 'utf8');
console.log('File contents:', text);
} catch (err) {
console.error('Could not read file:', err.message);
}
}
main();Two details matter here:
- The second argument
'utf8'tells Node to interpret the file as plain text. Without it, you get raw bytes instead of readable words. - The
try/catchblock catches failures, such as the file not existing. We will cover error handling in depth in a later chapter, but you need the basic pattern now.
Writing a file
Writing follows the same shape. This script creates or replaces a file:
// write.js
const { writeFile } = require('node:fs/promises');
async function main() {
try {
await writeFile('greeting.txt', 'Hello from Node.js!\n', 'utf8');
console.log('Saved.');
} catch (err) {
console.error('Could not write file:', err.message);
}
}
main();If greeting.txt does not exist, Node creates it. If it does exist, Node replaces its contents. Keep that in mind — there is no confirmation step.
Reading and writing together
Here is a small but realistic example: read one file, change it, and save the result elsewhere.
// copy.js
const { readFile, writeFile } = require('node:fs/promises');
async function main() {
try {
const text = await readFile('notes.txt', 'utf8');
const loud = text.toUpperCase();
await writeFile('loud-notes.txt', loud, 'utf8');
console.log('Created loud-notes.txt');
} catch (err) {
console.error('Something went wrong:', err.message);
}
}
main();diagram mermaid caption: The flow of an asynchronous file read alt: A sequence diagram showing a program asking the file system to read a file, continuing other work, and receiving the contents later when the read completes. sequenceDiagram participant P as Your program participant F as File system P->>F: readFile('notes.txt') Note over P: continues other work F-->>P: file contents (or error) Note over P: await resumes here
callout tip Always use the utf8 argument when you want text. Forgetting it is one of the most common beginner mistakes with the fs module — you will see strange output like <Buffer 48 65 6c 6c 6f> instead of "Hello".
Why non-blocking matters here
Reading a file from a disk takes a surprisingly long time in computer terms — often longer than millions of other operations. In a blocking style, a server waiting on one file cannot help any other visitor. In the non-blocking style, Node uses that waiting time for other tasks. For a single script it makes little difference; for a busy web server, it is the whole point of Node.js.
callout warning await only works inside an async function (or at the top level of a module). If you try to use it in a regular function, you will get a syntax error. Wrapping your file work in a main function, as the examples do, is the simplest habit to adopt.
Exercise
What happens if you call writeFile with the name of a file that already exists?
Show answer
Node replaces the file's entire contents with the new text. No warning is given, so be careful with important files.
Exercise
In the read.js example, why is the readFile call wrapped in try/catch?
Show answer
Because an asynchronous operation can fail — for example, the file might not exist. The catch block receives that error and lets the program report it instead of crashing.
Spotted a mistake, or code that does not work?