Cómo usar async/await en Node.js desde cero: qué es una promesa, cómo pasar de callbacks a promesas y cómo escribir código asíncrono que se lea de arriba a abajo.
Casi todo lo que hace Node.js tarda: leer un archivo, consultar la base de datos, llamar a una API. Mientras espera, Node sigue atendiendo otras cosas, y por eso el código asíncrono se escribe distinto al que ves en otros lenguajes. Antes eso se resolvía con callbacks anidados, y el resultado era código que nadie quería leer.
Hoy se resuelve con promesas y con async/await, y es la forma que vas a usar todos los días. Aquí vemos cómo usar async/await en Node.js desde lo más básico: qué es una promesa, cómo crear una, cómo convertir un callback viejo y qué hacen realmente async y await. Con esta base, el siguiente paso es ejecutar tareas en paralelo con Promise.all y aprender a manejar los errores.
Probé todo el código con Node.js 22. Los ejemplos usan módulos ES, así que pon "type": "module" en tu package.json (o usa archivos .mjs).
¿Qué es una promesa?
Una promesa es un objeto que representa un valor que todavía no tienes, pero que va a llegar (o a fallar). Tiene tres estados:
- Pendiente: sigue esperando.
- Cumplida: salió bien y ya tiene su valor.
- Rechazada: falló y tiene un error.
Eso es todo. async/await es una forma más cómoda de escribir código con promesas, y por dentro sigue siendo lo mismo.
Crear tu propia promesa
Casi nunca vas a crear promesas a mano, porque Node ya trae versiones con promesas de casi todo. Pero verlo una vez ayuda a entender cómo funcionan:
function dividir(a, b) {
return new Promise((resolve, reject) => {
if (b === 0) {
reject(new Error('No se puede dividir entre cero'));
} else {
resolve(a / b);
}
});
}
// Con .then / .catch / .finally
dividir(10, 2)
.then((resultado) => console.log('10 / 2 =', resultado))
.catch((err) => console.log('error:', err.message))
.finally(() => console.log('terminó el primer cálculo'));
dividir(10, 0)
.then((resultado) => console.log('10 / 0 =', resultado))
.catch((err) => console.log('error:', err.message))
.finally(() => console.log('terminó el segundo cálculo'));
La función que le pasas a new Promise recibe dos funciones: resolve para decir “salió bien, este es el valor” y reject para decir “falló, este es el error”. Después, con la promesa en la mano:
.then()se ejecuta si se cumplió..catch()se ejecuta si se rechazó..finally()se ejecuta en cualquier caso.
La salida es esta:
10 / 2 = 5 error: No se puede dividir entre cero terminó el primer cálculo terminó el segundo cálculo
Fíjate que las líneas van mezcladas: las dos operaciones corren a la vez y cada una avanza cuando le toca. Esa es la idea del código asíncrono.
De callbacks a promesas
Leamos el mismo archivo de tres maneras. Crea un datos.txt con cualquier texto y este código:
import { readFile } from 'node:fs';
import { promisify } from 'node:util';
import { readFile as readFilePromesa } from 'node:fs/promises';
// 1) Estilo viejo: callback
readFile('datos.txt', 'utf8', (err, texto) => {
if (err) {
console.log('callback -> error:', err.code);
return;
}
console.log('callback ->', texto.trim());
});
// 2) Convertir un callback en promesa con promisify
const readFileAsync = promisify(readFile);
readFileAsync('datos.txt', 'utf8')
.then((texto) => console.log('promisify ->', texto.trim()))
.catch((err) => console.log('promisify -> error:', err.code));
// 3) Hoy lo normal: fs/promises + async/await
async function leer() {
try {
const texto = await readFilePromesa('datos.txt', 'utf8');
console.log('async/await ->', texto.trim());
} catch (err) {
console.log('async/await -> error:', err.code);
}
}
leer();
Las tres hacen lo mismo. Lo que cambia es cómo se lee:
- El callback recibe el error como primer parámetro y tienes que acordarte de revisarlo siempre.
promisifyconvierte una función con callback en una que devuelve una promesa. Sirve para librerías viejas que no tienen versión con promesas.async/awaitse lee de arriba a abajo, como código normal, y los errores se atrapan contry/catch.
Para casi todo lo que viene en Node hay una versión /promises (fs/promises, timers/promises, dns/promises), así que casi nunca vas a necesitar promisify.
Qué hacen async y await
Son dos palabras con trabajos distintos:
asyncva antes de una función y hace que siempre devuelva una promesa, aunque dentro devuelvas un valor normal.awaitva antes de una promesa y pausa esa función hasta que se resuelva. El resto de tu programa sigue funcionando mientras tanto.
Se ve claro con un ejemplo mínimo:
javascript
async function saludar() {
return 'hola';
}
const resultado = saludar();
console.log(resultado); // una promesa, no el texto
console.log(await resultado); // ahora sí el texto
Promise { 'hola' }
hola
Aunque saludar devuelve un texto, desde fuera recibes una promesa. Para sacar el valor necesitas await. Y await solo funciona dentro de una función async o en el nivel principal de un módulo ES, que es lo que estamos usando en estos ejemplos.
Resultado: async/await en Node.js leyendo un archivo
Corre el ejemplo de callbacks, promisify y async/await con el archivo datos.txt en su lugar. Esto muestra la consola:
callback -> hola desde datos.txt promisify -> hola desde datos.txt async/await -> hola desde datos.txt
Ahora renombra o borra datos.txt y corre el mismo código:
callback -> error: ENOENT promisify -> error: ENOENT async/await -> error: ENOENT
Las tres formas dan el mismo resultado, tanto cuando el archivo existe como cuando no. ENOENT es el código de Node para “archivo o carpeta no encontrado”. La diferencia entre ellas es solo cómo se escribe y qué tan fácil es de leer cuando el código crece: con callbacks, cada paso extra mete otro nivel de anidación; con async/await, solo agregas otra línea.
Errores comunes
1. Imprimir una promesa en lugar del valor
Si te sale Promise { <pending> } o Promise { 'algo' } en la consola, casi seguro olvidaste el await. Estás viendo la promesa y no lo que contiene. Pasa más en cadenas largas, cuando llamas a una función async y usas el resultado como si ya estuviera listo.
2. await fuera de una función async
Si tu proyecto usa require (CommonJS) y pones await en el nivel principal, Node te dice:
SyntaxError: await is only valid in async functions and the top level bodies of modules
Tienes dos salidas: poner el await dentro de una función async, o pasar tu proyecto a módulos ES con "type": "module" en el package.json.
3. Mezclar demasiado .then() con await
No es un error, pero se vuelve difícil de leer. Elige uno por función. Para la mayoría de los casos, async/await es más claro y deja los errores en un solo try/catch.
(Si tienes alguna anécdota real con callbacks anidados de algún proyecto viejo, este es el lugar para contarla en uno o dos párrafos.)
Siguientes pasos
Ya sabes qué es una promesa y cómo escribir código con async/await. Lo que sigue es lo que más se usa en la práctica:
- Promise.all en Node.js: cómo ejecutar varias tareas a la vez y ahorrar tiempo.
- Manejo de errores en Node.js: cómo evitar que un error sin atrapar tire tu servidor.
Y si quieres ver async en acción con archivos grandes, mira cómo leer CSV en Node.js con streams.
Recomendaciones
- Usa
async/awaitpara casi todo y deja.then()para cadenas cortas en las que de verdad te sirva. - Cuando uses async/await en Node.js, busca primero si lo que necesitas ya trae versión con promesas (
fs/promises,timers/promises). Es más limpio que convertirlo tú. - Si tienes que convertir un callback viejo, usa
promisifyantes que escribir tu propia promesa a mano. - Una función
asyncsiempre devuelve una promesa. Acuérdate de esperarla conawaitdonde la llames.
