Archivos
En resumen: Un archivo guarda los datos en el disco incluso después de que el programa termina; en Node.js el módulo node:fs lee y escribe archivos, y los flujos leen un archivo grande línea por línea.
Problema
El programa olvida
Estás escribiendo un programa para tus gastos. Los anotas por la noche, lo cierras, y al día siguiente no sabe nada de ayer. Aquí está el ejemplo más corto:
let visits = 0;visits++;console.log(`Ejecutado ${visits} vez.`);Ejecutado 1 vez.Da igual cuántas veces lo ejecutes: siempre dice «1 vez». Las variables viven en la memoria mientras el programa funciona. Cuando termina, la memoria se libera y todo desaparece.
Para recordar, el programa tiene que escribir los datos donde se quedan después de que termine: en un archivo en el disco.
La idea
Un archivo con node:fs
Aquí tienes un programa completo que escribe una nota en un archivo y la vuelve a leer:
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("notes.txt", "Comprar leche\nPagar la luz antes del 15\n"); const text = readFileSync("notes.txt", "utf8"); process.stdout.write(text);} catch (e) { console.log(`Problema con el archivo: ${e.message}`);}Comprar lechePagar la luz antes del 15import { readFileSync, writeFileSync } from "node:fs";toma dos funciones del módulonode:fs(file system, «el sistema de archivos»). Viene con Node y no se instala.writeFileSync(nombre, texto)escribe el texto y crea el archivo si no existe."notes.txt"es un archivo con ese nombre en la carpeta desde la que se ejecutó el programa.readFileSync(nombre, "utf8")lee todo el archivo como texto."utf8"es la codificación: cómo los bytes se convierten en letras.- «Sync» significa que el programa espera hasta que el archivo se escribe o se lee, y solo entonces sigue.
- Si algo sale mal (el disco está lleno, el archivo está bloqueado o no existe), la función lanza un error. JavaScript no te obliga a capturarlo, pero sin
catchel programa se detiene. process.stdout.write(text)imprime el texto tal cual.console.logañadiría un salto de línea más al final.
Ejecuta el programa y abre la carpeta: notes.txt está ahí, y puedes abrirlo con cualquier editor de texto. La próxima ejecución lo encontrará.
En cada ejemplo que sigue, los import están arriba: del módulo tomas solo las funciones que usas.
La idea
Añade y lee las líneas
Los gastos se añaden uno por uno. appendFileSync añade al final, en lugar de escribir el archivo desde cero. split("\n") divide en líneas el texto leído:
import { appendFileSync, readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("expenses.txt", "Almuerzo;12.50\n"); appendFileSync("expenses.txt", "Taxi;8.20\n"); appendFileSync("expenses.txt", "Libro;18.90\n"); const lines = readFileSync("expenses.txt", "utf8").trimEnd().split("\n"); let total = 0; for (const line of lines) { const parts = line.split(";"); total += Number(parts[1]); console.log(`${parts[0]}: ${parts[1]} €`); } console.log(`${lines.length} gastos, total ${total.toFixed(2)} €`);} catch (e) { console.log(`Problema con el archivo: ${e.message}`);}Almuerzo: 12.50 €Taxi: 8.20 €Libro: 18.90 €3 gastos, total 39.60 €lines es un array de cadenas, una por cada línea. trimEnd() antes de split quita el último salto de línea. Sin él, el array tendría al final un elemento más: una cadena vacía "".
Más precisamente, para curiosos
appendFileSynccrea el archivo si no existe, así que la primera vez que añades no hay error.- Sin
"utf8",readFileSyncdevuelve unBuffer: los bytes del archivo tal cual, por ejemplo<Buffer 43 61 66 c3 a9 ...>. Solo se convierte en texto con la codificación. - También hay una versión en la que el programa no se queda esperando al disco:
import { readFile } from "node:fs/promises";yconst text = await readFile("expenses.txt", "utf8");. Los servidores usan esa. En programas cortos,readFileSynces más sencillo. readFileSynclee todo el archivo en la memoria. Para un archivo de varios gigabytes es mejor leer línea por línea. Mira la tarjeta sobre los flujos.
Pruébalo
Dos turnos
¿Qué imprimirá el programa?
import { readFileSync, writeFileSync } from "node:fs";
try { writeFileSync("shift.txt", "Primer turno: Carlos\n"); writeFileSync("shift.txt", "Segundo turno: Lucía\n"); process.stdout.write(readFileSync("shift.txt", "utf8"));} catch (e) { console.log(e.message);}writeFileSync borra el contenido anterior y escribe el nuevo. La primera línea ya no está, incluso antes de la lectura. Es la razón más común de «se me borró el archivo».
La idea
Cuando el archivo no está
La configuración del programa está en config.txt, y alguien lo borró. Si nadie captura la excepción:
import { readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
console.log(readConfig());node:fs:440 return binding.readFileUtf8(path, stringToFlags(options.flag)); ^
Error: ENOENT: no such file or directory, open 'config.txt' at readFileSync (node:fs:440:20) at readConfig (file:///app/app.js:4:10) at file:///app/app.js:7:13 ... { errno: -2, code: 'ENOENT', syscall: 'open', path: 'config.txt'}Arriba, Node muestra la línea de su propio código interno donde se lanzó el error. Después viene el error en sí: ENOENT (error, no entry) es el código de «no existe el archivo o la carpeta». Las líneas con «at» van de dentro hacia fuera: readFileSync, después readConfig en la línea 4, llamada desde la línea 7. Las líneas de Node que vienen después están abreviadas con «...».
Los errores de node:fs son del tipo Error, y se distinguen por e.code. Revisa el código en catch:
import { existsSync, readFileSync } from "node:fs";
function readConfig() { return readFileSync("config.txt", "utf8");}
try { console.log(readConfig());} catch (e) { if (e.code === "ENOENT") { console.log(`No hay archivo ${e.path}. Uso la configuración por defecto.`); } else { console.log(`No puedo leer la configuración: ${e.message}`); }}console.log(existsSync("config.txt"));No hay archivo config.txt. Uso la configuración por defecto.falsee.path es el nombre del archivo que falta. existsSync(path) comprueba si el archivo existe. La comprobación no reemplaza a catch: el archivo puede desaparecer entre la comprobación y la lectura, y hay otras razones por las que la lectura puede fallar.
La idea
Línea por línea y flujos
writeFileSync escribe todo el texto de una vez. Para escribir por partes y para leer línea por línea están los flujos (streams). Un flujo mantiene el archivo abierto mientras trabajas con él, así que hay que cerrarlo al final:
import { createReadStream, createWriteStream } from "node:fs";import { once } from "node:events";import { createInterface } from "node:readline";
try { const out = createWriteStream("report.txt"); for (let day = 1; day <= 3; day++) { out.write(`Día ${day}: ${day * 120} €\n`); } out.end(); await once(out, "close");} catch (e) { console.log(`No puedo escribir: ${e.message}`);}
try { const lines = createInterface({ input: createReadStream("report.txt") }); for await (const line of lines) { console.log(line); }} catch (e) { console.log(`No puedo leer: ${e.message}`);}Día 1: 120 €Día 2: 240 €Día 3: 360 €createWriteStreamabre el archivo para escribir.writele da el texto al flujo. El salto de línea no aparece solo: el\nlo escribes tú.end()dice «eso es todo» y empieza el cierre.await once(out, "close")espera hasta que el flujo escribe todo y cierra el archivo.onceviene del módulonode:events.createInterfacedenode:readlinelee línea por línea el flujo del archivo, yfor awaitda las líneas una por una, sin el carácter de salto de línea. Es lo mismo que usas para leer del teclado.
Cerrar no es un trámite: hasta que el flujo se cierra, el texto puede estar todavía en su búfer en la memoria, no en el disco. Para que el código sea corto, aquí no hay try:
import { createWriteStream, readFileSync, statSync } from "node:fs";import { once } from "node:events";
const out = createWriteStream("report.txt");for (let day = 1; day <= 3; day++) { out.write(`Día ${day}: ${day * 120} €\n`);}console.log(`En el búfer: ${out.writableLength} bytes`);out.end();await once(out, "close");console.log(`En el archivo: ${statSync("report.txt").size} bytes`);for (const line of readFileSync("report.txt", "utf8").trimEnd().split("\n")) { console.log(line);}Paso a paso, en palabras
- node:fs da el flujo y el tamaño del archivo, y node:events la espera. A la izquierda está la memoria del programa, a la derecha el disco.
- createWriteStream crea un flujo con un búfer en la memoria. El archivo report.txt se abre sin que el programa espere.
- write le da el texto al flujo, y el texto espera en el búfer. Al disco todavía no llega nada.
- writableLength: 48 bytes esperan en el búfer. En el disco no hay ni uno.
- end() cierra el flujo, y await espera a que eso ocurra. El búfer va al disco: el archivo pasa a tener 48 bytes.
- 48 bytes para 39 caracteres con los saltos de línea: en UTF-8 cada letra latina sin tilde ocupa 1 byte, la «í» ocupa 2 y el € ocupa 3.
- readFileSync vuelve a leer las tres líneas del disco.
JavaScript no tiene un try (...) que cierre un recurso solo. readFileSync y writeFileSync no lo necesitan, porque abren y cierran el archivo ellos mismos. Cuando eres tú quien mantiene el archivo abierto, el cierre va en finally, de la lección sobre finally:
import { closeSync, openSync, writeSync } from "node:fs";
const fd = openSync("report.txt", "w");try { writeSync(fd, "Día 1: 120 €\n");} finally { closeSync(fd);}openSync abre el archivo y devuelve su número (fd, file descriptor, «descriptor de archivo»). writeSync escribe directamente en el archivo, sin búfer. closeSync lo cierra, pase lo que pase en el try.
Más precisamente, para curiosos
- El lenguaje va a tener
using: un recurso que se cierra solo al final del bloque, comotry (...)en Java. Node 22 todavía no lo reconoce y da unSyntaxError. - Búfer. El flujo reúne el texto en la memoria (
out.writableLengthes cuántos bytes esperan) y lo envía al disco sin que el programa se detenga a esperar. Por eso necesitas unawaitdespués deend(). readlinesobrecreateReadStreames la forma de leer un archivo muy grande: en la memoria solo está la parte actual. Aquí está por separado:
import { createReadStream, writeFileSync } from "node:fs";import { createInterface } from "node:readline";
writeFileSync("report.txt", "Día 1: 120 €\nDía 2: 240 €\n");const lines = createInterface({ input: createReadStream("report.txt") });for await (const line of lines) { console.log(line);}Pruébalo
¿Cuándo se cierra?
JavaScript no tiene try (...), así que el recurso se cierra en finally. Aquí está la impresora de la caja de la lección sobre finally: close() está en el finally del try interior, y el error lo captura el exterior:
¿Qué imprimirá el programa?
class Printer { constructor() { console.log("La impresora está ocupada."); }
close() { console.log("La impresora está libre."); }}
try { const printer = new Printer(); try { console.log("Imprimiendo el recibo..."); const qty = Number("tres"); if (Number.isNaN(qty)) { throw new Error("tres no es un número"); } console.log(`Cantidad: ${qty}`); } finally { printer.close(); }} catch (e) { console.log("Cantidad incorrecta.");}Number("tres") da NaN sin ningún error, así que la comprobación lanza el error ella misma. La línea «Cantidad» se salta. El finally del try interior se ejecuta enseguida, incluso antes del catch exterior. Por eso «libre» aparece antes de «Cantidad incorrecta.» Cuando catch se ejecuta, la impresora ya está liberada.Pruébalo
Encuentra el error
1, pero hay dos líneas. ¿En qué línea está el error?Elige la línea con el error.
end() solo empieza el cierre. La escritura ocurre más tarde, mientras que readFileSync lee enseguida, cuando en el archivo solo está la línea de encabezado. El programa no da ningún error: cuenta mal sin avisar. Después de out.end(); añade await once(out, "close"); (once viene de node:events): entonces el flujo está cerrado y hay dos líneas.La idea
Carpetas
Las copias de seguridad de la caja van a una carpeta, una por día:
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";import { join } from "node:path";
const backup = "backup";try { mkdirSync(join(backup, "old"), { recursive: true }); writeFileSync(join(backup, "2026-10-01.csv"), "Café,2.40\n"); writeFileSync(join(backup, "2026-10-02.csv"), "Café,2.40\nTé,1.80\n"); for (const name of readdirSync(backup).sort()) { const p = join(backup, name); if (statSync(p).isDirectory()) { console.log(`${name}/`); } else { console.log(`${name} (${statSync(p).size} bytes)`); } } unlinkSync(join(backup, "2026-10-01.csv")); console.log(existsSync(join(backup, "2026-10-01.csv")));} catch (e) { console.log(e.message);}2026-10-01.csv (11 bytes)2026-10-02.csv (20 bytes)old/false| Función | Qué hace |
|---|---|
mkdirSync(p, { recursive: true }) |
crea la carpeta y todas las carpetas por encima de ella, si no existen |
join(dir, "nombre") (de node:path) |
la ruta a nombre dentro de dir |
statSync(p).isDirectory(), existsSync(p) |
si es una carpeta, si existe |
statSync(p).size |
el tamaño en bytes |
unlinkSync(p) |
borra un archivo (rmdirSync(p) borra una carpeta vacía) |
readdirSync(dir) |
un array con los nombres de todo lo que hay en la carpeta |
Los tamaños son 11 y 20 bytes, aunque los caracteres son menos: en UTF-8, la «é» de «Café» y de «Té» ocupa 2 bytes.
readdirSync no promete ningún orden para los nombres, así que los ordenamos con sort().Más precisamente, para curiosos
rmdirSyncborra solo una carpeta vacía. Si no, lanza un error con el códigoENOTEMPTY.rmSync(p, { recursive: true })borra la carpeta con todo lo que tiene dentro, así que ten cuidado con él.copyFileSync(origen, destino)copia un archivo, yrenameSync(origen, destino)lo mueve o le cambia el nombre.joinpone entre las partes el separador del sistema:/en Linux y macOS,\en Windows. Por eso las rutas no se construyen con+.- La ruta
"notes.txt"es relativa: a la carpeta desde la que se ejecutó el programa."/home/ana/notes.txt"es absoluta.resolve("notes.txt")denode:pathmuestra exactamente dónde está el archivo.
Ejercicio
Programas que recuerdan
Resuelve las tareas en tu editor. En cada tarea, el programa primero crea él mismo el archivo de entrada con writeFileSync, para que puedas probarlo enseguida.
1. Un archivo de configuración. Crea server.conf con este contenido, después léelo e imprime cada ajuste como clave: valor. Sáltate las líneas vacías y las líneas que empiezan con #. Pista: busca el primer = con indexOf y toma las dos partes con slice. split("=", 2) no sirve aquí: en JavaScript descarta todo lo que hay después del segundo =.
# configuración del servidormax_connections=500log_file_path=/var/log/server.log
db_timeout=30max_connections: 500log_file_path: /var/log/server.logdb_timeout: 302. Un filtro de archivo de registro. Crea app.log con cuatro líneas (dos INFO y dos ERROR). Lee un nivel del teclado, por ejemplo ERROR, y escribe las líneas con ese nivel en errors.log línea por línea: lee con readline, escribe con un flujo (createWriteStream). Imprime errors.log y cuántas líneas escribiste.
ERRORERROR: Failed to load configurationERROR: Null pointer exceptionEscribí 2 líneas en errors.log3. El contenido de una carpeta. Crea una carpeta project con los archivos document.txt, image.jpg, script.js y la carpeta backup. Escribe sus nombres, ordenados alfabéticamente, en directory_contents.txt, con una / después de los nombres de las carpetas. Imprime el archivo.
backup/document.txtimage.jpgscript.js4. Un archivo índice. Crea text.txt con las tres líneas de abajo. Lee una palabra del teclado y escribe en index.txt cada lugar donde aparece, en la forma línea:posición: el número de línea (desde 1) y el índice dentro de la línea (desde 0). Si la palabra está dos veces en una línea, son dos entradas.
hello worldworld hellohello again, hellohello1:02:63:03:13Más tareas
- Contador de ejecuciones. Arregla el programa de la primera tarjeta: cuenta las ejecuciones en
visits.txt. Si el archivo no existe, empieza desde 0. - Buscar y reemplazar en un archivo. La tarea de la lección «Expresiones regulares: grupos y reemplazo»: lee un archivo, reemplaza «viejo» por «nuevo» y escribe el resultado en un archivo nuevo.
- Fechas de un archivo. La tarea de las fechas de la lección «Expresiones regulares»: lee las fechas de un archivo, una por línea.
- Una copia con números de línea. Lee un archivo y escribe una copia en la que cada línea empieza con su número:
1: ...,2: ....
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Interfaces sin interface · Excepciones · try, catch y finally · Cadenas
- Continúa en: Datos en archivos · Códigos hash y funciones hash · Programación en red · Bases de datos
- Ejercicios sobre el tema: Texto y archivos