Las solicitudes se cuelgan cuando una Promesa nunca se resuelve o rechaza: falta una devolución de llamada, un reject olvidado, un cliente de BD abandonado o un interbloqueo esperando por sí mismo. En Express 5, el cliente agota el tiempo de espera mientras el servidor mantiene las conexiones y los slots del pool.
Tarjeta de receta de referencia rápida: lista para copiar y pegar.
// MAL: Promesa manual a la que le falta la ruta de rechazofunction fetchLegacy(): Promise<string> { return new Promise((resolve) => { legacyLib.getData((err: Error | null, data: string) => { if (!err) resolve(data); // la ruta de error nunca rechaza - se cuelga indefinidamente en caso de error }); });}// BIEN: envuelve con resolve + rejectfunction fetchLegacyFixed(): Promise<string> { return new Promise((resolve, reject) => { legacyLib.getData((err: Error | null, data: string) => { if (err) reject(err); else resolve(data); }); });}
// Envoltorio de timeoutasync function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> { let timer: NodeJS.Timeout; const timeout = new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms); }); try { return await Promise.race([p, timeout]); } finally { clearTimeout(timer!); }}
Cuándo usarlo:
Picos de latencia p95 con CPU plana (solicitudes esperando, no calculando)
El recuento de conexiones abiertas crece mientras el RPS es constante
La prueba de integración se cuelga hasta el timeout de Jest
Después de envolver librerías de estilo callback en Promesas
import express from "express";import { Pool } from "pg";const pool = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });const app = express();// BUG: el cliente nunca se libera si la consulta lanza un error antes de la liberación en el orden incorrectoapp.get("/orders-bug/:id", async (req, res) => { const client = await pool.connect(); const result = await client.query("SELECT * FROM orders WHERE id = $1", [req.params.id]); if (!result.rows[0]) { // se olvidó client.release() en el retorno anticipado - el pool se agota con el tiempo return res.status(404).json({ error: { code: "NOT_FOUND" } }); } client.release(); res.json({ data: result.rows[0] });});// SOLUCIÓN: try/finally siempre liberaapp.get("/orders/:id", async (req, res, next) => { const client = await pool.connect(); try { const result = await client.query("SELECT * FROM orders WHERE id = $1", [req.params.id]); if (!result.rows[0]) return res.status(404).json({ error: { code: "NOT_FOUND" } }); res.json({ data: result.rows[0] }); } finally { client.release(); }});// BUG: falta await - se dispara y se olvida, los errores no se manejanapp.post("/notify-bug", (req, res) => { sendEmail(req.body.email); // devuelve una Promesa, no se espera res.status(202).json({ accepted: true });});async function sendEmail(to: string) { await new Promise((r) => setTimeout(r, 100)); if (!to.includes("@")) throw new Error("invalid email");}// SOLUCIÓNapp.post("/notify", async (req, res, next) => { try { await sendEmail(req.body.email); res.status(202).json({ accepted: true }); } catch (err) { next(err); }});
Lo que esto demuestra:
El retorno anticipado sin client.release() filtra conexiones del pool
try/finally garantiza la liberación incluso en un 404
La falta de await en los manejadores de rutas asíncronas causa rechazos no manejados
Los errores asíncronos de Express 5 necesitan next(err) o un envoltorio
1. node --inspect + pausa durante el cuelgue - inspeccionar la pila asíncrona2. Registrar el pool activo: pool.totalCount, pool.idleCount, pool.waitingCount3. trace_unhandled_rejections en staging4. Añadir withTimeout alrededor de las llamadas externas sospechosas
Rama de resolución única del constructor de Promesas - Las API de callback que dan error silenciosamente se cuelgan. Solución: Siempre reject(err).
forEach con callback asíncrono - No espera; carrera y errores perdidos. Solución:for...of con await o Promise.all.
Inicio de transacción sin commit/rollback - Conexión mantenida hasta el timeout. Solución:try { ... commit } catch { rollback } finally { release }.
Interbloqueo en la misma conexión - Consulta await mientras la transacción está abierta en el mismo cliente incorrectamente intercalada. Solución: Un flujo secuencial por cliente.
Ignorar pool.waitingCount - La métrica alcanza 10+ mientras idleCount es 0. Solución: Liberar clientes; reducir la concurrencia.