Ir al contenido
wannacode

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.`);
Salida
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:

notes.js
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}`);
}
Salida
Comprar leche
Pagar la luz antes del 15
  • import { readFileSync, writeFileSync } from "node:fs"; toma dos funciones del módulo node: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 catch el programa se detiene.
  • process.stdout.write(text) imprime el texto tal cual. console.log añ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}`);
}
Salida
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
  • appendFileSync crea el archivo si no existe, así que la primera vez que añades no hay error.
  • Sin "utf8", readFileSync devuelve un Buffer: 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"; y const text = await readFile("expenses.txt", "utf8");. Los servidores usan esa. En programas cortos, readFileSync es más sencillo.
  • readFileSync lee 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);
}
Respuesta

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:

app.js
import { readFileSync } from "node:fs";
function readConfig() {
return readFileSync("config.txt", "utf8");
}
console.log(readConfig());
Error
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"));
Salida
No hay archivo config.txt. Uso la configuración por defecto.
false

e.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}`);
}
Salida
Día 1: 120 €
Día 2: 240 €
Día 3: 360 €
  • createWriteStream abre el archivo para escribir. write le da el texto al flujo. El salto de línea no aparece solo: el \n lo 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. once viene del módulo node:events.
  • createInterface de node:readline lee línea por línea el flujo del archivo, y for await da 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
  1. 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.
  2. createWriteStream crea un flujo con un búfer en la memoria. El archivo report.txt se abre sin que el programa espere.
  3. write le da el texto al flujo, y el texto espera en el búfer. Al disco todavía no llega nada.
  4. writableLength: 48 bytes esperan en el búfer. En el disco no hay ni uno.
  5. end() cierra el flujo, y await espera a que eso ocurra. El búfer va al disco: el archivo pasa a tener 48 bytes.
  6. 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.
  7. 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, como try (...) en Java. Node 22 todavía no lo reconoce y da un SyntaxError.
  • Búfer. El flujo reúne el texto en la memoria (out.writableLength es cuántos bytes esperan) y lo envía al disco sin que el programa se detenga a esperar. Por eso necesitas un await después de end().
  • readline sobre createReadStream es 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.");
}
Respuesta

Pruébalo

Encuentra el error

El programa añade un pedido debajo de la línea de encabezado y cuenta las líneas del archivo. Imprime 1, pero hay dos líneas. ¿En qué línea está el error?

Elige la línea con el error.

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);
}
Salida
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
  • rmdirSync borra solo una carpeta vacía. Si no, lanza un error con el código ENOTEMPTY. 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, y renameSync(origen, destino) lo mueve o le cambia el nombre.
  • join pone 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") de node:path muestra 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 =.

server.conf
# configuración del servidor
max_connections=500
log_file_path=/var/log/server.log
db_timeout=30
Salida
max_connections: 500
log_file_path: /var/log/server.log
db_timeout: 30

2. 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.

Entrada
ERROR
Salida
ERROR: Failed to load configuration
ERROR: Null pointer exception
Escribí 2 líneas en errors.log

3. 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.

directory_contents.txt
backup/
document.txt
image.jpg
script.js

4. 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.

text.txt
hello world
world hello
hello again, hello
Entrada
hello
index.txt
1:0
2:6
3:0
3:13

Má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

1. ¿Qué devuelve readFileSync("notes.txt") sin "utf8"?
2. writeFileSync(file, "A\n"); y después writeFileSync(file, "B\n"); ¿Qué hay en el archivo?
3. ¿Tiene JavaScript (Node 22) un try (...) que cierre el archivo solo?
4. readFileSync no encuentra el archivo. ¿Cómo sabes que el problema es justo ese?
5. Escribes con createWriteStream, llamas a end() y enseguida lees el archivo con readFileSync. ¿Qué puede haber en él?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·