~/blog · 3 de mayo de 2026 · 4 min de lectura
Escribir ficheros sin romperlos: el truco de tmp + rename
Un proceso que muere a mitad de un fs.writeFile deja un JSON corrupto. No es un caso raro — pasa en deploys, en reinicios, en cortes de luz. Hay una solución de dos líneas.
Cuando construí FileSystemAdapter para cron-scheduler-ts, lo primero que hice fue un fs.writeFile directo. Funcionaba en local, pasaba los tests. Luego pensé: ¿qué pasa si el proceso muere justo cuando está escribiendo? Y me puse nervioso.
fs.writeFile abre el fichero, lo trunca a cero bytes, y luego escribe el contenido nuevo. Si el proceso muere entre el truncado y el write completo, el fichero tiene 0 bytes o un JSON a medias. En el siguiente arranque, JSON.parse lanza un error, el scheduler no puede cargar el estado, y pierdes todos los jobs persistidos. Para una tarea de backup de las 2 de la mañana que no se volvía a ejecutar hasta el día siguiente — eso es un problema real.
El problema con la escritura directa
// ❌ No seguro — hay una ventana donde data.json queda corrupto
// Si el proceso muere aquí ↓ el fichero tiene 0 bytes o JSON roto
await fs.writeFile("data.json", JSON.stringify(estado, null, 2));
// Si esto falla a mitad → data.json está corrupto para siemprePor qué rename() es diferente
En sistemas POSIX — Linux, macOS — rename() es una operación atómica. El sistema operativo garantiza que, desde el punto de vista del sistema de ficheros, o el rename ocurre completamente o no ocurre. No hay estado intermedio. Esto significa que si escribes a un fichero temporal y luego haces rename a la ruta final, el lector siempre ve o el fichero antiguo completo o el nuevo completo. Nunca uno roto.
// ✅ Escritura atómica — data.json nunca queda corrupto
async function guardarEstado(ruta: string, estado: unknown) {
const rutaTmp = ruta + ".tmp";
// Escribimos en el temporal — si falla, data.json sigue intacto
await fs.writeFile(rutaTmp, JSON.stringify(estado, null, 2), "utf8");
// rename() es atómica: en este punto, data.json = nuevo estado completo
await fs.rename(rutaTmp, ruta);
}escritura atómica
Cuándo lo necesitas de verdad
La respuesta honesta: no siempre. Para logs, usar append es suficientemente seguro y mucho más eficiente. Para ficheros que se leen una vez al arrancar y representan estado crítico — configuración, estado de jobs, metadatos de usuario — el patrón tmp + rename es imprescindible. En Windows también funciona desde Vista: la API MoveFileEx tiene el mismo comportamiento atómico.
Lo que sí puedo decir con seguridad: desde que lo implementé en FileSystemAdapter, no he tenido ni un solo reporte de fichero corrupto. Y eso que el paquete se reinicia constantemente durante los tests. Dos líneas extra, cero problemas.