Leer y escribir archivos de texto en JavaScript (Node.js)

Con Node.js, JavaScript puede leer y escribir archivos del disco. Todo lo necesario está en el módulo node:fs/promises, que viene de serie: no hay que instalar nada.

Escribir un archivo

Guarda esto como escribir.mjs. La extensión .mjs le dice a Node que es un módulo moderno, y eso permite usar import y await directamente, sin envolverlos en una función.

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

await writeFile("notas.txt", "Primera línea\nSegunda línea\n", "utf8");
await appendFile("notas.txt", "Tercera línea, añadida después\n", "utf8");
console.log("notas.txt escrito");
node escribir.mjs
salidanotas.txt escrito

writeFile crea el archivo o lo sobrescribe entero si ya existía. appendFile añade al final sin tocar lo anterior. El tercer argumento, "utf8", es la codificación: con ella los acentos y las eñes se guardan bien.

Leer el archivo entero

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

const texto = await readFile("notas.txt", "utf8");
console.log(texto);

const lineas = texto.split("\n").filter((l) => l !== "");
console.log(`${lineas.length} líneas; la última: «${lineas.at(-1)}»`);
salidaPrimera línea
Segunda línea
Tercera línea, añadida después

3 líneas; la última: «Tercera línea, añadida después»

readFile devuelve todo el contenido en una sola cadena. Para trabajar por líneas basta con split("\n"); el filter quita la línea vacía que deja el último salto de línea.

Sin "utf8" no recibes texto, sino bytes. Si lo olvidas, readFile devuelve un Buffer:

salida<Buffer 50 72 69 6d 65 72 61 20 6c c3 ad 6e 65 61 0a 53 65 6 ...

Fíjate en que «í» ocupa dos bytes (c3 ad): en UTF-8, las letras con acento ocupan más de uno.

Leer línea a línea

Si el archivo es grande, no conviene cargarlo entero en memoria. readLines() lo entrega línea a línea, y for await las recorre a medida que llegan:

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

const archivo = await open("notas.txt");
let n = 0;
for await (const linea of archivo.readLines()) {
  n++;
  console.log(`${n}: ${linea}`);
}
salida1: Primera línea
2: Segunda línea
3: Tercera línea, añadida después

readFile carga el archivo entero en memoria; readLines lo lee línea a líneaLeer un archivo entero o línea a líneareadFile()el archivoen discotodo el textoen memoria a la veztu códigoCómodo y rápido. Con archivos de cientos de MB, la memoria se dispara.readLines()el archivoen discouna líneacada veztu códigoGasta lo mismo con 1 KB que con 10 GB: cada línea se procesa y se suelta.

readFile carga todo el archivo a la vez; readLines trae una línea, la procesas, y pasa a la siguiente. Esquema: decodigo.com.

Escribir mucho: un flujo

Para generar un archivo grande, en lugar de componer una cadena enorme y guardarla de golpe, se abre un flujo de escritura y se le va pasando el texto:

import { createWriteStream } from "node:fs";
import { once } from "node:events";

const salida = createWriteStream("numeros.txt", "utf8");
for (let i = 1; i <= 100_000; i++) {
  if (!salida.write(`${i}\n`)) {
    await once(salida, "drain");
  }
}
salida.end();
await once(salida, "finish");
console.log("100,000 líneas escritas sin cargarlas en memoria");
salida100,000 líneas escritas sin cargarlas en memoria

Cuando write devuelve false, el búfer interno está lleno y conviene esperar al evento drain antes de seguir. Así la memoria no crece aunque escribas millones de líneas. El archivo resultante tiene, efectivamente, 100,000 líneas.

El error más frecuente: el archivo no existe

Si lees un archivo que no está, Node lanza una excepción y, si nadie la captura, el programa termina:

errorError: ENOENT: no such file or directory, open 'no-existe.txt'
    at async open (node:internal/fs/promises:1345:25)
    ...
  code: 'ENOENT',

Lo útil es el code: ENOENT significa «no existe tal archivo o carpeta». Se captura con try/catch y se decide qué hacer, por ejemplo usar valores por omisión. Los demás errores, como la falta de permisos (EACCES), se relanzan para no esconderlos:

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

try {
  const texto = await readFile("config.txt", "utf8");
  console.log(texto);
} catch (err) {
  if (err.code === "ENOENT") {
    console.log("No hay config.txt; uso los valores por omisión");
  } else {
    throw err;
  }
}
salidaNo hay config.txt; uso los valores por omisión

Cuál usar

Lo que necesitasLo que conviene
Leer un archivo de configuración o pequeñoreadFile(ruta, "utf8")
Escribir un archivo de una vezwriteFile(ruta, texto, "utf8")
Añadir líneas a un registroappendFile
Recorrer un archivo grandeopen(ruta) y for await (const l of archivo.readLines())
Generar un archivo grandecreateWriteStream, respetando drain
¿Y readFileSync? Las versiones Sync bloquean el programa hasta terminar. En un guion corto da igual, pero en un servidor detienen a todos los usuarios mientras se lee el disco. Con fs/promises y await el código se lee igual de fácil y no bloquea nada.

Todo el código de esta página se ejecutó con Node.js 24.22 en un contenedor limpio antes de publicarla; las salidas, incluidos los errores, están copiadas de esa ejecución. Documentación oficial: API de fs/promises de Node.js.