Promisificación en JavaScript
Aprende la promisificación en JavaScript: envuelve callbacks en una Promise, crea un helper promisify() genérico, gestiona callbacks multi-argumento y conoce sus limitaciones. Ejemplos ejecutables.
¿Qué es la promisificación?
La promisificación consiste en envolver una función basada en callbacks para que devuelva una Promise en lugar de recibir un callback. Se hace una vez y a partir de entonces se puede usar .then(), .catch(), encadenamiento y async/await en una función que nunca fue diseñada para ello.
Esta página explica cómo envolver una API de callback error-first, cómo construir un helper genérico reutilizable promisify(), cómo manejar callbacks que devuelven más de un resultado y los casos en que la promisificación no funciona.
¿Por qué promisificar?
Las API antiguas de JavaScript y la mayor parte de la biblioteca estándar de Node.js comunican sus resultados a través de un callback que se les pasa. Ese estilo se anida rápidamente y dispersa el manejo de errores:
getUser(id, (err, user) => {
if (err) return handleError(err);
getOrders(user, (err, orders) => {
if (err) return handleError(err);
getTotal(orders, (err, total) => {
if (err) return handleError(err);
console.log(total);
});
});
});Si las mismas funciones devolvieran promises, la lógica se aplanaría en una única cadena lineal (o unas pocas líneas await) con un solo .catch() para todo el flujo. La promisificación es el puente entre estos dos mundos — consulta Callbacks y más allá para conocer el lado de los callbacks.
La convención callback error-first
Antes de envolver cualquier cosa, es necesario conocer la forma que se va a envolver. Los callbacks de Node.js siguen la convención error-first (o "estilo Node"): el callback es el último argumento y se invoca como callback(error, result).
- En caso de fallo,
errores un objetoErroryresultes undefined. - En caso de éxito,
erroresnullyresultcontiene el valor.
La promisificación mapea esto directamente: un error distinto de null se convierte en reject(error), y un result exitoso se convierte en resolve(result).
Envolver una API de callback individual
Este es el patrón principal. Se envuelve una función de estilo callback en una nueva Promise, llamando a reject para el error y a resolve para el valor. El ejemplo simula una API error-first con setTimeout para que funcione en cualquier lugar, incluido el navegador:
El wrapper acepta el mismo argumento id, lo reenvía y proporciona su propio callback que sirve de puente hacia resolve/reject. El código que llama a la función ya no ve ningún callback.
Usar el wrapper con async/await
El verdadero beneficio de una función que devuelve una promise es que funciona con async/await, convirtiendo el código asíncrono en algo que se lee de arriba a abajo:
Un helper promisify() genérico
Escribir un wrapper a mano para cada función resulta repetitivo. Un helper genérico toma cualquier función error-first y devuelve una versión que retorna una promise. El truco consiste en recopilar todos los argumentos originales con un parámetro rest y luego añadir nuestro propio callback:
Dado que el helper usa ...args y reenvía this, funciona con funciones que tengan cualquier número de argumentos iniciales. En Node.js, la biblioteca estándar incluye exactamente esto como util.promisify, por lo que rara vez necesitarás escribir el tuyo propio:
const fs = require('fs');
const util = require('util');
const readFile = util.promisify(fs.readFile);
readFile('file.txt', 'utf8')
.then(data => console.log(data))
.catch(err => console.error(err));Manejar callbacks con múltiples argumentos
El helper simple asume que el callback entrega un único resultado: callback(err, result). Algunas API pasan varios valores, como callback(err, header, body). Un resolve(result) simple descartaría silenciosamente todo lo que venga después del primer valor.
Una promise solo puede resolverse con un valor, así que recoge los argumentos adicionales en un array (o un object) y resuélvelo con eso:
util.promisify de Node admite la misma idea a través de un símbolo personalizado (util.promisify.custom), pero para funciones ad-hoc un array es el enfoque más sencillo.
Limitaciones y advertencias
La promisificación es mecánica, pero tiene límites reales:
- Espera la convención error-first. Si una función señala errores de otra manera — por ejemplo, con un retorno boolean, una excepción lanzada o un orden
(result, err)— un helper genérico lo interpretará mal. En esos casos, envuélvelas manualmente. - Solo gestiona una única finalización. Las promises se resuelven una sola vez. Una función que invoca su callback de forma repetida (eventos, streams,
setInterval, un callback de progreso) no puede promisificarse — solo la primera llamada resolvería la promise; las llamadas posteriores se ignoran. Usa una API de eventos o un iterador asíncrono para valores repetidos. - No se puede cancelar una promise. Si la API de callback subyacente admite cancelación (como limpiar un temporizador), esa capacidad se pierde una vez que queda oculta tras una promise.
- El wrapper cambia la firma de llamada. Los invocadores deben usar ahora
.then/awaiten lugar de pasar un callback. No promisifiques una función que algún código sigue llamando en estilo callback sin mantener ambas versiones. - Un throw dentro del executor sigue rechazando. El código que se ejecuta de forma síncrona dentro de
new Promise((resolve, reject) => { ... })se captura y se convierte en un rechazo — pero un error lanzado después dentro de un callback asíncrono no se captura automáticamente, razón por la cual debes llamar areject(err)explícitamente.
Buenas prácticas
- Promisifica en el límite. Convierte las API de I/O y temporizadores una sola vez, cerca del punto donde entran en tu código, y mantén el resto de tu base de código basada en promises.
- Prefiere los built-ins. En Node.js, recurre a
util.promisify(o a los módulosfs/promises,dns/promises, etc.) antes de escribir un wrapper propio. - Maneja siempre el rechazo. Añade un
.catch()o envuelveawaitentry/catch; un rechazo no gestionado puede hacer que un proceso de Node se cierre inesperadamente. - Mantén nombres predecibles. Una convención habitual es añadir el sufijo
Asynca la versión con promise (readFileAsync) para que ambos estilos puedan coexistir.
Temas relacionados
- JavaScript Promise — el objeto que se crea al promisificar.
- Encadenamiento de Promises — secuencia llamadas promisificadas de forma limpia.
- Async/Await — la sintaxis que hace que las funciones promisificadas se lean como código síncrono.
- Callbacks y más allá — el patrón desde el que se está convirtiendo.
- Promise API — combina varias llamadas promisificadas con
Promise.ally similares.