Volver a explorar Snippets

Manejo Robusto de Fallos Críticos y Apagado Elegante en Node.js

JAVASCRIPT 17 de junio de 2026 4 lecturas
Implementa una estrategia avanzada para interceptar errores críticos no capturados en Node.js, asegurando un apagado elegante del servidor y la liberación segura de recursos de base de datos, evitando interrupciones abruptas y corrupción de estado.

Cuando un servidor Node.js en producción se encuentra con un error asíncrono no capturado fuera de un bloque try/catch, el proceso entra en un estado inestable. Permitir que el servidor continúe operando en un estado corrupto es extremadamente peligroso, pero terminar el contenedor de inmediato interrumpe las transacciones HTTP activas de nuestros usuarios y deja conexiones huérfanas en el pool de la base de datos.

El Antipatrón Común: Reinicios Abruptos vs. Silencio de Errores

He observado que muchos desarrolladores cometen el error de simplemente ignorar los eventos globales de error o de invocar un process.exit(1) de forma inmediata. Esto provoca que las peticiones que estaban a mitad de camino se queden colgadas y que nuestra base de datos relacional sufra por bloqueos de tablas no liberados. La solución de nivel sénior requiere interceptar el fallo, rechazar nuevas peticiones, terminar el trabajo pendiente con un tiempo límite (timeout) y cerrar los sockets de forma limpia.

Implementación del Handler de Apagado Elegante (Graceful Shutdown)

Este snippet implementa lo que yo llamo un "búnker perimetral" de resiliencia. Escucha los eventos críticos del ciclo de vida del proceso de Node.js, interactúa con el servidor de Express y libera de forma atómica el pool de conexiones de la base de datos antes de devolver el código de salida correspondiente. Mi objetivo es asegurar que, incluso frente a un fallo crítico, nuestra aplicación se apague de la manera más controlada y menos disruptiva posible.

const express = require('express');
const { Pool } = require('pg');

const app = express();
const pool = new Pool({ connectionString: process.env.DATABASE_URL });

// Ejemplo de ruta que puede generar un error asíncrono no capturado
app.get('/error', (req, res, next) => {
    // Simula un error asíncrono no capturado
    Promise.reject(new Error('¡Error asíncrono no manejado!'));
});

// Ejemplo de ruta que puede generar un error síncrono no capturado
app.get('/sync-error', (req, res, next) => {
    throw new Error('¡Error síncrono no manejado!');
});

app.get('/', (req, res) => {
    res.send('Servidor funcionando. Intenta /error o /sync-error para probar el shutdown.');
});

const server = app.listen(process.env.PORT || 3000, () => {
    console.log(`Servidor corriendo de forma segura en http://localhost:${process.env.PORT || 3000}`);
});

// --- EL SNIPPET DE CONTROL PERIMETRAL --- 

function initializeResilienceBunker(httpServer, dbPool) {
    let shutdownInProgress = false;
    let shutdownTimeoutId = null;

    const initiateGracefulShutdown = async (errorSource, errorDetails) => {
        if (shutdownInProgress) {
            console.warn(`⚠️ Intento de apagado duplicado desde [${errorSource}]. Ignorando.`);
            return;
        }
        shutdownInProgress = true;
        console.error(`🚨 ALERTA CRÍTICA [${errorSource}]:`, errorDetails instanceof Error ? errorDetails.message : errorDetails);
        if (errorDetails instanceof Error && errorDetails.stack) {
            console.error(errorDetails.stack);
        }

        // 1. Detener el servidor HTTP para dejar de aceptar nuevas conexiones
        console.log('🛑 Iniciando cierre del servidor HTTP. No se aceptarán más peticiones.');
        httpServer.close(async (err) => {
            if (err) {
                console.error('❌ Error al cerrar el servidor HTTP:', err);
            } else {
                console.log('✅ Servidor HTTP cerrado exitosamente.');
            }

            try {
                // 2. Liberar de forma segura el pool de conexiones de la DB
                console.log('📦 Drenando y cerrando pool de PostgreSQL...');
                await dbPool.end();
                console.log('✅ Pool de PostgreSQL drenado y cerrado limpiamente.');
            } catch (errDb) {
                console.error('❌ Error al cerrar recursos de la DB:', errDb);
            } finally {
                // Si todo salió bien, limpiar el timeout forzado y salir
                if (shutdownTimeoutId) {
                    clearTimeout(shutdownTimeoutId);
                }
                console.log('🚪 Proceso terminado con código de salida 1.');
                process.exit(1); // Salida por error controlado
            }
        });

        // Forced Exit Fallback: Si el drenado tarda más de 10s, forzar salida para evitar bucles zombies
        shutdownTimeoutId = setTimeout(() => {
            console.error('⚠️ Forzando salida: El cierre de recursos excedió el tiempo límite (10s).');
            process.exit(1);
        }, 10000);
    };

    // Capturar promesas rechazadas que no tienen un .catch()
    process.on('unhandledRejection', (reason, promise) => {
        initiateGracefulShutdown('unhandledRejection', reason);
    });

    // Capturar errores síncronos volados en el hilo principal
    process.on('uncaughtException', (error) => {
        initiateGracefulShutdown('uncaughtException', error);
    });

    // Capturar señales de terminación (e.g., Ctrl+C, SIGTERM de orquestadores)
    process.on('SIGTERM', () => {
        initiateGracefulShutdown('SIGTERM', 'Señal de terminación recibida.');
    });

    process.on('SIGINT', () => {
        initiateGracefulShutdown('SIGINT', 'Señal de interrupción (Ctrl+C) recibida.');
    });
}

// Inyección del búnker al arrancar la app
initializeResilienceBunker(server, pool);
¿Qué te pareció?
🔥 Brillante 0
💡 Me sirvió 0
🚀 A otro nivel 0