I/O#

JavaScript’s I/O surface depends on the host. In Node (and Deno / Bun in Node-compat mode), reach for node:fs/promises for files, node:stream for streams, console and process.stdin/stdout/stderr for the standard streams, JSON for serialisation, and Buffer / typed arrays for binary data. In the browser, fetch, File, Blob, ReadableStream, and FormData cover the same ground.

This page leans on Node since that is where most CLI / server work happens. For networking I/O, see Networking. For CLI argument handling, see CLI.

Files#

node:fs/promises is the async surface. The synchronous mirror (readFileSync, writeFileSync) lives on node:fs and avoid it outside startup code.

import {readFile, writeFile, appendFile, rename} from "node:fs/promises";

const data = await readFile("config.json", "utf8");
await writeFile("out.txt", "hello\n");
await appendFile("log.txt", `${Date.now()}\n`);
await rename("tmp.json", "config.json");

Pass "utf8" (or the encoding) for text; omit it to get a Buffer.

const raw = await readFile("logo.png");      // Buffer
const txt = await readFile("notes.md", "utf8");

Paths#

node:path joins and resolves; import.meta.url and fileURLToPath locate the current module.

import {join, dirname, resolve} from "node:path";
import {fileURLToPath} from "node:url";

const __dirname = dirname(fileURLToPath(import.meta.url));
const configPath = join(__dirname, "config.json");

Streams#

Streams move data without buffering the whole payload. The operator uses createReadStream for large files and pipeline to chain stages.

import {createReadStream} from "node:fs";
import {createInterface} from "node:readline";

const rl = createInterface({input: createReadStream("/var/log/syslog")});
for await (const line of rl) {
  if (line.includes("error")) console.log(line);
}

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

await pipeline(
  createReadStream("input.txt"),
  createGzip(),
  createWriteStream("input.txt.gz"),
);

Standard streams#

process.stdin, process.stdout, process.stderr are the Node-side streams. console.log writes to stdout; console.error writes to stderr.

import {createInterface} from "node:readline";

for await (const line of createInterface({input: process.stdin})) {
  process.stdout.write(line.toUpperCase() + "\n");
}

console.error("debug:", obj);

JSON#

JSON.parse and JSON.stringify are the surface. The operator passes a reviver on parse to transform values, and a replacer plus an indent on stringify for safe / pretty output.

const obj = JSON.parse('{"name":"rk","ts":"2026-01-01"}', (k, v) =>
  k === "ts" ? new Date(v) : v
);

const pretty = JSON.stringify(obj, null, 2);

const safe = JSON.stringify(obj, (k, v) =>
  k === "password" ? undefined : v          // drop secrets
);

JSON.stringify throws on circular references and silently drops functions, undefined, and symbol values. For complex structures (Map, Set, Date, typed arrays, cycles) use structuredClone or a library (superjson).

Binary data#

Node’s Buffer is a subclass of Uint8Array with hex / base64 / utf-8 helpers. The operator stays on Buffer for Node code and on Uint8Array for portable code.

const buf = Buffer.from("hello", "utf8");
buf.toString("hex");              // "68656c6c6f"
buf.toString("base64");           // "aGVsbG8="

const fromHex = Buffer.from("48656c6c6f", "hex");

const u8 = new Uint8Array([0x48, 0x69]);
const text = new TextDecoder("utf-8").decode(u8);
const bytes = new TextEncoder().encode(text);

For binary parsing, DataView reads typed values out of an ArrayBuffer with explicit endianness.

const view = new DataView(buf.buffer);
const port = view.getUint16(0, false);    // big-endian

Console#

console.log / error / warn / info / debug / trace write structured output. console.table is useful for arrays of objects; console.dir shows objects with depth.

console.table([{ip: "1.2.3.4", port: 80}, {ip: "5.6.7.8", port: 443}]);
console.dir(deep, {depth: null, colors: true});

References#