fs.appendFile()
Appends data to the end of a file, creating the file first if it does not exist.
fs.appendFile(path, data[, options], callback)- Added in Node.js v0.6.7
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | <string> | <Buffer> | <URL> | <integer> | Filename or file descriptor. A descriptor opened for appending is not closed for you, and a descriptor opened for reading fails. |
$data | <string> | <Buffer> | The bytes to add at the end of the file. Include your own newline — nothing is added for you. |
$optionsoptional | <Object> | <string>= { encoding: 'utf8', mode: 0o666, flag: 'a' } | A string is treated as the encoding. As an object: encoding <string>|<null> (default 'utf8'), mode <integer> (default 0o666), flag <string> (default 'a'), flush <boolean> (default false), and signal <AbortSignal> to abort an in-progress append. |
$callback | <Function> | Called as (err), null on success. |
Return value
undefinedNothing. Success or failure arrives as the callback's err argument.
Errors
Reports EACCES, ENOENT for a missing parent directory, ENOSPC and EBADF (descriptor not opened for writing) through the callback; throws synchronously on an invalid data type or a missing callback.
Changelog
- PHP v21.1.0, v20.10.0The flush option is supported, running fs.fsync() before the descriptor is closed.
- PHP v18.0.0Passing an invalid callback now throws ERR_INVALID_ARG_TYPE instead of ERR_INVALID_CALLBACK.
- PHP v14.0.0data no longer coerces unsupported input to strings.
- PHP v10.0.0The callback parameter is no longer optional; omitting it throws a TypeError at runtime.
- PHP v7.0.0The options object passed in is never modified.
- PHP v5.0.0path may be a file descriptor.
fs.appendFile() opens the path with the 'a' flag, writes at the end, and closes it. If the file does not exist it is created, so you never need an existence check or a “touch” step first. This is the straightforward way to add a line to a log, an audit trail or an NDJSON stream without reading and rewriting the whole file.
What it does not add
No newline is inserted for you. appendFile(f, 'a') followed by appendFile(f, 'b') gives you ab, not two lines. End every record with '\n' yourself.
Cost and concurrency
Each call is a full open–write–close cycle. That is fine for occasional records and wasteful for thousands per second — for a hot logging path keep one fs.createWriteStream() open instead. On POSIX, writes smaller than the pipe buffer through the O_APPEND flag are effectively atomic against other appenders, which is why several processes can share a log file without interleaving mid-line; Windows offers no such guarantee.
Note that appending never shrinks a file. Rotating or truncating is a separate job, usually fs.truncate() or a rename plus a fresh file.
Example
When to use it
- Add a timestamped line to an application log file as events happen.
- Accumulate NDJSON records from a stream of results, one JSON object per line.
- Record an audit entry after each privileged action without rewriting the whole file.
Frequently asked questions
Does fs.appendFile create the file if it does not exist?
Yes. It uses the 'a' flag, which creates the file when it is missing, so no existence check or pre-creation step is needed. Parent directories are still not created — those must already exist.
Why is all my appended text on one line?
Because nothing adds a line break for you. Append the newline yourself, for example fs.appendFile(file, line + '\n', cb), or join records with '\n' before writing.
Should I use fs.appendFile or a write stream for logging?
Use fs.appendFile for occasional records; use fs.createWriteStream(path, { flags: 'a' }) for anything high-volume. Each appendFile call reopens and recloses the file, so thousands per second waste far more time in syscalls than in actual writing.
More examples
It creates the file when it is missing
There is no file before the call and a one-line file afterwards. Because the 'a' flag implies create, you can skip the existsSync guard that people commonly write around appends.
const fs = require('node:fs');
fs.appendFile('fresh.log', 'appendFile creates the file if it is missing\n', (err) => {
if (err) throw err;
console.log('exists now:', fs.existsSync('fresh.log'));
console.log(fs.readFileSync('fresh.log', 'utf8').trim());
fs.unlinkSync('fresh.log');
});Building a log line by line
Three sequential appends produce three lines, confirmed by the count of 3. Each iteration opens and closes the file again, which is exactly the overhead a write stream would avoid in a high-volume logger.
import fs from 'node:fs';
import fsp from 'node:fs/promises';
async function main() {
const lines = ['GET /', 'GET /about', 'POST /login'];
for (const line of lines) {
await fsp.appendFile('access.log', line + '\n');
}
const contents = await fsp.readFile('access.log', 'utf8');
console.log('entries:', contents.trimEnd().split('\n').length);
console.log(contents.trimEnd());
await fsp.unlink('access.log');
console.log('removed:', !fs.existsSync('access.log'));
}
main();Appending binary data
Buffers are appended byte for byte with no encoding step and no separator, so 'AB' plus 0x43 gives the three bytes 414243, decoding to 'ABC'. This is the concatenation behaviour that surprises people writing text records without a trailing newline.
const fs = require('node:fs');
fs.appendFile('bytes.log', Buffer.from('AB'), (err) => {
if (err) throw err;
fs.appendFile('bytes.log', Buffer.from([0x43]), (err2) => {
if (err2) throw err2;
const buf = fs.readFileSync('bytes.log');
console.log('length:', buf.length);
console.log('hex:', buf.toString('hex'));
console.log('text:', buf.toString('utf8'));
fs.unlinkSync('bytes.log');
});
});
Discussion