Grokking Node.js
Vote

0% completed

​

Reading and Writing Files with the File System Module

Creating the File with writeFile

Adding Items with appendFile

Reading the List with readFile

Deleting the File with unlink

The Versions That Wait

Choosing Between Them

Running Steps in Order with Promises

const fs = require('fs'); fs.readFile('todo.txt', 'utf8', (err, data) => { console.log('File content:', data); }); console.log('Read requested');
Read requested File content: Buy milk

The file todo.txt held one line, Buy milk, and the last line of the script printed first. fs.readFile starts the read and returns at once, the script continues, and the callback runs later with the contents of the file. Most functions in the fs module work this way, and most of them also have a second version that waits.

The fs module is built into Node.js, so you only need to load it:

const fs = require('fs');

Each script in this lesson starts with that line, and the examples below leave it out to save space. Applications use the module to write logs, store uploaded content, and read configuration. This lesson builds one small example, a to-do list kept in a text file named todo.txt, one item per line. It creates the file, adds items to it, reads it, and deletes it, first with callbacks, then with the versions that wait, and finally with promises.

Creating the File with writeFile

fs.writeFile writes text to a file. If the file does not exist, it creates the file; if the file exists, it replaces everything in it.

// write.js fs.writeFile('todo.txt', 'Buy milk\n', (err) => { if (err) { console.log('Error writing file:', err.message); return; } console.log('todo.txt saved'); });
todo.txt saved

The callback receives one argument, an error object. It is null when the write succeeded, and it describes the problem when the write failed, for example when the folder does not exist or the program may not write there. The \n at the end of the text is a newline, so that the next item starts on a new line.

The file name here is relative, so Node.js resolves it against the folder that you run the command from. Run these scripts from the folder that contains them, or build the path with path.join(__dirname, 'todo.txt').

Adding Items with appendFile

Calling writeFile again would replace the list. To add an item to the end of the file and keep what is already there, use fs.appendFile:

// append.js fs.appendFile('todo.txt', 'Call the bank\n', (err) => { if (err) { console.log('Error appending to file:', err.message); return; } console.log('Item added'); });

fs.appendFile also creates the file if it does not exist yet, so a log file never needs to be created first. Running write.js once and append.js twice leaves three lines in the file. Running write.js again leaves only Buy milk.

Reading the List with readFile

fs.readFile reads the whole file. The 'utf8' argument names the character encoding, so that the callback receives a string. Without it, the callback receives a Buffer, an object that holds the raw bytes of the file.

// read.js fs.readFile('todo.txt', 'utf8', (err, data) => { if (err) { console.log('Error reading file:', err.code); return; } console.log(data); });

After one write and two appends, it prints:

Buy milk Call the bank Call the bank

The callback receives the error first and the data second. This order, with the error always as the first argument, is called an error-first callback, and almost every callback in Node.js follows it.

When the file is missing, err.code is ENOENT, short for "error, no entry". The error object also has a message with the full path of the missing file, which is useful in a log. Checking err.code lets the program react to one kind of failure, for example by treating a missing to-do file as an empty list.

fs.readFile loads the entire file into memory. That is fine for a to-do list or a configuration file. For a file of several gigabytes, Node.js offers streams, which read a file in small pieces.

fs.unlink deletes a file. The name comes from the Unix system call that removes a file's name from its folder.

// del.js fs.unlink('todo.txt', (err) => { if (err) { console.log('Error deleting file:', err.code); return; } console.log('todo.txt deleted'); });

The first run prints todo.txt deleted. A second run prints Error deleting file: ENOENT, because there is nothing left to delete.

The Versions That Wait

Each function above has a synchronous twin whose name ends in Sync. A synchronous function does not return until the operation has finished. It returns the result directly instead of passing it to a callback, and it reports a failure by throwing an error, so you catch it with try...catch:

// sync.js try { fs.writeFileSync('todo.txt', 'Buy milk\n'); fs.appendFileSync('todo.txt', 'Call the bank\n'); const data = fs.readFileSync('todo.txt', 'utf8'); console.log(data); fs.unlinkSync('todo.txt'); fs.readFileSync('todo.txt', 'utf8'); } catch (err) { console.log('File operation failed:', err.code); }
Buy milk Call the bank File operation failed: ENOENT

The steps run in the order they are written, and each one starts only after the one before it has finished. That makes the code easy to follow. The last read fails because the file was just deleted, and the error stops the try block and jumps to catch.

The cost is that the main thread does nothing else while it waits for the disk. A short script does not care. A web server does, because while one request waits for readFileSync, every other request waits too.

Choosing Between Them

Use the synchronous functions where nothing else needs to run at the same time: in small scripts, in command-line tools, and while a server starts up, for example to read its configuration file before it accepts the first request.

Use the asynchronous functions in any code that runs while a server is handling requests, so that the main thread stays free for the other requests while the disk works.

Running Steps in Order with Promises

The callback versions have a problem of their own. Two calls written one after the other, such as writeFile followed by readFile, both start at once, so the read may happen before the write has finished. To run them in order with callbacks, each step must go inside the callback of the step before it, and four steps produce four levels of nesting.

The fs/promises module provides the same functions in a form that returns promises. Inside an async function, await pauses that function until the operation finishes, but it does not block the main thread:

// promises.js const fs = require('fs/promises'); async function main() { try { await fs.writeFile('todo.txt', 'Buy milk\n'); await fs.appendFile('todo.txt', 'Call the bank\n'); const data = await fs.readFile('todo.txt', 'utf8'); console.log(data); await fs.unlink('todo.txt'); console.log('todo.txt deleted'); } catch (err) { console.log('File operation failed:', err.code); } } main();
Buy milk Call the bank todo.txt deleted

The code reads in order like the synchronous version, and it handles errors with the same try...catch. The difference is that while each await waits, the event loop can run other callbacks. For new code, this is usually the best of the three forms. The next chapter explains promises and async/await in detail.

Reading Progress

0%


Vote for new content

On This Page

Creating the File with writeFile

Adding Items with appendFile

Reading the List with readFile

Deleting the File with unlink

The Versions That Wait

Choosing Between Them

Running Steps in Order with Promises