Earlier in this chapter, we wrote a series of Node.js programs that could watch files for changes. Now let’s explore Node.js’s methods for reading and writing files. Along the way we’ll see two common error-handling patterns in Node.js: error events on EventEmitters and err callback arguments.
There are a few approaches to reading and writing files in Node. The simplest is to read in or write out the entire file at once. This technique works well for small files. Other approaches read and write by creating Streams or staging content in a Buffer. Here’s an example of the whole-file-at-once approach:
| | 'use strict'; |
| | const fs = require('fs'); |
| | fs.readFile('target.txt', (err, data) => { |
| | if (err) { |
| | throw err; |
| | } |
| | console.log(data.toString()); |
| | }); |
Save this file as read-simple.js and run it with node:
| | $ node read-simple.js |
You’ll see the contents of target.txt echoed to the command line. If the file is empty, all you’ll see is a blank line.
Notice how the first parameter to the readFile() callback handler is err. If readFile is successful, then err will be null. Otherwise the err parameter will contain an Error object. This is a common error-reporting pattern in Node.js, especially for built-in modules. In our example’s case, we throw the error if there was one. Recall that an uncaught exception in Node.js will halt the program by escaping the event loop.
The second parameter to our callback, data, is a Buffer—the same kind that was passed to our various callbacks in previous sections.
Writing a file using the whole-file approach is similar. Here’s an example:
| | 'use strict'; |
| | const fs = require('fs'); |
| | fs.writeFile('target.txt', 'hello world', (err) => { |
| | if (err) { |
| | throw err; |
| | } |
| | console.log('File saved!'); |
| | }); |
This program writes hello world to target.txt (creating it if it doesn’t exist, or overwriting it if it does). If for any reason the file can’t be written, then the err parameter will contain an Error object.
You create a read stream or a write stream by using fs.createReadStream() and fs.createWriteStream(), respectively. For example, here’s a very short program called cat.js. It uses a file stream to pipe a file’s data to standard output:
| | #!/usr/bin/env node |
| | 'use strict'; |
| | require('fs').createReadStream(process.argv[2]).pipe(process.stdout); |
Because the first line starts with #!, you can execute this program directly in Unix-like systems. You don’t need to pass it into the node program (although you still can).
Use chmod to make it executable:
| | $ chmod +x cat.js |
Then, to run it, send the name of the chosen file as an additional argument:
| | $ ./cat.js target.txt |
| | hello world |
The code in cat.js does not bother assigning the fs module to a variable. The require() function returns a module object, so we can call methods on it directly.
You can also listen for data events from the file stream instead of calling pipe(). The following program called read-stream.js does this:
| | 'use strict'; |
| | require('fs').createReadStream(process.argv[2]) |
| | .on('data', chunk => process.stdout.write(chunk)) |
| | .on('error', err => process.stderr.write(`ERROR: ${err.message}\n`)); |
Here we use process.stdout.write() to echo data, rather than console.log. The incoming data chunks already contain any newline characters from the input file. We don’t need the extra newline that console.log would add.
Conveniently, the return value of on() is the same emitter object. We take advantage of this fact to chain our handlers, setting up one right after the other.
When working with an EventEmitter, the way to handle errors is to listen for error events. Let’s trigger an error to see what happens. Run the program, but specify a file that doesn’t exist:
| | $ node read-stream.js no-such-file |
| | ERROR: ENOENT: no such file or directory, open 'no-such-file' |
Since we’re listening for error events, Node.js invokes our handler (and then proceeds to exit normally). If you don’t listen for error events, but one happens anyway, Node.js will throw an exception. And as we saw before, an uncaught exception will cause the process to terminate.
The file-access methods we’ve discussed in this chapter so far are asynchronous. They perform their I/O duties—waiting as necessary—completely in the background, only to invoke callbacks later. This is by far the preferred way to do I/O in Node.
Even so, many of the methods in the fs module have synchronous versions, as well. These end in *Sync, like readFileSync, for example. Doing synchronous file access might look familiar to you if you haven’t done a lot of async development in the past. However, it comes at a substantial cost.
When you use the *Sync methods, the Node.js process will block until the I/O finishes. This means Node.js won’t execute any other code, won’t trigger any callbacks, won’t process any events, won’t accept any connections—nothing. It’ll just sit there indefinitely waiting for the operation to complete.
However, synchronous methods are simpler to use since they lack the callback step. They either return successfully or throw an exception, without the need for a callback function. There actually are cases where this style of access is OK; we’ll discuss them in the next section.
Here’s an example of how to read a file using the readFileSync() method:
| | const fs = require('fs'); |
| | const data = fs.readFileSync('target.txt'); |
| | process.stdout.write(data.toString()); |
The return value of readFileSync() is a Buffer—the same as the parameter passed to callbacks of the asynchronous readFile() method we saw before.
Node.js’s fs module has many other methods that map nicely onto POSIX conventions. (POSIX is a family of standards for interoperability between operating systems—including filesystem utilities.)[20] To name a few examples, you can copy() files and unlink() (delete) them. You can use chmod() to change permissions and mkdir() to create directories.
These functions rely on the same kinds of callback parameters we’ve used in this chapter. They’re all asynchronous by default, but many come with equivalent *Sync versions.