Skip to content
Node.js nd fs 5 min read

Writing & Appending Files

Writing data to disk is one of the most common things a Node.js program does — saving configuration, emitting logs, exporting reports, or persisting uploads. The fs module gives you three distinct tools for the job: writeFile for replacing a file’s contents in one shot, appendFile for adding to the end of a file, and createWriteStream for feeding large or continuous data without holding it all in memory. Picking the right one, and understanding the flags that govern how a file is opened, is what keeps your writes correct and your memory usage flat.

Writing a whole file with writeFile

fs.writeFile (and its promise counterpart in node:fs/promises) replaces a file’s entire contents with the data you pass. If the file does not exist it is created; if it does, it is truncated first. This is the right tool when you already have the complete value in memory — a JSON document, a rendered template, a small text file.

import { writeFile } from "node:fs/promises";

const config = { port: 3000, logLevel: "info" };

await writeFile("config.json", JSON.stringify(config, null, 2), "utf8");
console.log("config.json saved");

Output:

config.json saved

The third argument is the options object (or an encoding shortcut string). When you pass a string as the data, the encoding defaults to "utf8", so the "utf8" above is explicit rather than strictly required.

Appending with appendFile

fs.appendFile writes to the end of a file instead of overwriting it, creating the file first if it is missing. It is the natural fit for log files and audit trails where each call should add a line without disturbing what came before.

import { appendFile } from "node:fs/promises";

const line = `${new Date().toISOString()} request handled\n`;
await appendFile("access.log", line, "utf8");

Each call opens the file, seeks to the end, writes, and closes it. That is fine for occasional appends, but for a high-frequency log you should keep a write stream open instead (see below) rather than paying the open/close cost on every line.

appendFile is essentially writeFile with the flag set to "a". Anything you can express with appendFile you can also express with writeFile and { flag: "a" }.

The flag option

Both writeFile and appendFile accept a flag in their options object that controls how the underlying file is opened. The flag decides whether existing content is kept, replaced, or whether the operation should fail when the file already exists.

FlagMeaningIf file existsIf file missing
wWrite (default for writeFile)Truncated to empty, then writtenCreated
aAppend (default for appendFile)Kept, data added to endCreated
wxWrite, exclusiveFails with EEXISTCreated
axAppend, exclusiveFails with EEXISTCreated
r+Read/writeOpened, not truncatedFails with ENOENT

The exclusive flags are useful when you must not clobber an existing file — for example, claiming a lock file or writing a one-time setup marker:

import { writeFile } from "node:fs/promises";

try {
  await writeFile("install.lock", "locked", { flag: "wx" });
  console.log("Lock acquired");
} catch (err) {
  if (err.code === "EEXIST") {
    console.log("Already installed, skipping");
  } else {
    throw err;
  }
}

Output:

Lock acquired

Writing buffers vs strings

You can hand writeFile either a string or a Buffer (or any TypedArray). When you pass a string, Node encodes it using the encoding option — "utf8" by default. When you pass a Buffer, the bytes are written verbatim and the encoding option is ignored, which is exactly what you want for binary data like images or compressed payloads.

import { writeFile } from "node:fs/promises";

// String: encoded as UTF-8 text
await writeFile("note.txt", "Héllo, world", "utf8");

// Buffer: raw bytes, no encoding applied
const bytes = Buffer.from([0x89, 0x50, 0x4e, 0x47]);
await writeFile("magic.bin", bytes);

Reach for a Buffer whenever the data is not human-readable text. Forcing binary data through a string encoding can silently corrupt it.

Streaming large writes with createWriteStream

writeFile buffers the whole payload in memory before writing it. For large files, or for data that arrives incrementally (an HTTP download, generated rows, a transform pipeline), use fs.createWriteStream. A write stream lets you push chunks over time and only holds a small buffer at any moment.

import { createWriteStream } from "node:fs";

const stream = createWriteStream("report.csv", { flags: "w" });

stream.write("id,name\n");
for (let i = 1; i <= 1000; i++) {
  stream.write(`${i},user-${i}\n`);
}
stream.end();

stream.on("finish", () => console.log("report.csv written"));
stream.on("error", (err) => console.error("Write failed:", err.code));

Output:

report.csv written

Note that the stream option is named flags (plural) and uses the same values from the table above. Always call stream.end() when you are done, and listen for finish to know the data is fully flushed to disk. For piping a readable source into a file, prefer stream/promisespipeline, which handles errors and cleanup for you:

import { pipeline } from "node:stream/promises";
import { createWriteStream } from "node:fs";

const res = await fetch("https://example.com/large.zip");
await pipeline(res.body, createWriteStream("large.zip"));
console.log("download complete");

Mixing manual write() calls with pipeline on the same stream leads to interleaved, corrupt output. Pick one approach per stream.

CommonJS consumers import these the same way with require:

const { createWriteStream } = require("node:fs");

Best Practices

  • Use writeFile when the full contents fit comfortably in memory; switch to createWriteStream for large or incremental data.
  • Use appendFile (or flag "a") for logs so you never truncate existing entries.
  • Reach for the wx/ax exclusive flags when overwriting an existing file would be a bug, and handle the EEXIST error.
  • Pass a Buffer for binary data and a string with explicit "utf8" for text — never round-trip binary through a text encoding.
  • Always end() a write stream and listen for both finish and error; prefer pipeline when copying from a readable source.
  • Keep a single long-lived write stream for high-frequency logging instead of calling appendFile per line.
Last updated June 14, 2026
Was this helpful?