Async/await vs. callbacks vs. promises/futures: ¿qué cambia realmente por debajo?
Los tres resuelven el mismo problema (haz esto después de que termine aquello), pero cada uno cambia dónde vuelve el control, cómo se propagan los errores y qué aspecto tiene la pila de llamadas cuando algo falla.
APRENDE HACIENDOPon a prueba tu intuición3 pasos
Async/await vs. callbacks vs. promises/futures: ¿qué cambia realmente por debajo?
Usa los ejemplos y la explicación de la lección para comprobar tu idea.
Reflexión personal. No se envía a ningún servicio de IA.#No son tres mecanismos que compiten entre sí
Vale la pena preguntarse, mentalmente, antes de responder: async/await, promises y callbacks no son tres formas distintas de lograr concurrencia. En un runtime de un solo hilo como Node.js, debajo de los tres hay exactamente un mecanismo: el event loop, llamadas al sistema no bloqueantes y una cola de planificación de tareas. Callbacks, promises y async/await son tres capas de sintaxis construidas sobre ese único mecanismo, cada una resolviendo un problema real de ergonomía o seguridad que tenía la capa anterior.
Entender "qué cambia por debajo" significa entender a qué se reduce realmente cada capa, no solo cuál se ve más prolija.
#Callbacks: una referencia a función, nada más
Un callback es la forma más primitiva: pasas una referencia a una función a algo que la llamará más adelante, una vez que una operación termine. No hay ningún objeto que represente "el resultado eventual", solo la promesa informal de que alguien llamará tu función eventualmente, con una convención de primer argumento de error si sigues la tradición de Node.
function getUserCallback(id, callback) {db.query('SELECT * FROM users WHERE id = ?', [id], function (err, user) {if (err) return callback(err);db.query('SELECT * FROM orders WHERE user_id = ?', [user.id], function (err2, orders) {if (err2) return callback(err2);callback(null, { user: user, orders: orders });});});}
El problema no es estético, es de correctitud: cada callback tiene que recordar revisar su propio error, y nada obliga a eso. Olvidar un solo if (err) hace que el error pase silenciosamente, o peor, que corran ambas ramas. El anidamiento también dificulta salir temprano con return o compartir lógica de limpieza: esta es la "pirámide de la perdición" de la que todos se quejan, pero el costo real es lo fácil que resulta meter un bug ahí adentro.
#Promises: un objeto real, y una cola real
Una promise es un objeto con una máquina de estados interna (pending → fulfilled o rejected, y solo puede transicionar una vez). Como es un objeto, se puede retornar, guardar, componer con Promise.all y encadenar con .then/.catch: el runtime, no quien la llama, es responsable de invocar tu continuación una vez que el estado se resuelve.
function getUserPromise(id) {return db.query('SELECT * FROM users WHERE id = ?', [id]).then(function (user) {return db.query('SELECT * FROM orders WHERE user_id = ?', [user.id]).then(function (orders) {return { user: user, orders: orders };});});}
Aquí es donde el detalle del event loop importa de verdad en una entrevista: los callbacks de una promise (las funciones que pasas a .then) corren en la cola de microtareas, no en la misma cola que los timers o los callbacks de I/O (la cola de macrotareas). Después de que termina cada bloque síncrono de código, el runtime de JavaScript vacía toda la cola de microtareas antes de mirar la siguiente macrotarea. Ese orden es preciso y determinista, y es justo por eso que este fragmento clásico imprime en un orden específico y nada obvio:
console.log('A');setTimeout(function () { console.log('B'); }, 0);Promise.resolve().then(function () { console.log('C'); });console.log('D');// Imprime: A, D, C, B
#Async/await: las mismas promises, con ropa secuencial
async/await no introduce un nuevo primitivo de concurrencia. Una función async siempre devuelve una promise, y await es azúcar sintáctica para: suspender la ejecución de esta función en este punto, registrar el resto de la función como continuación sobre la promise esperada (planificada como microtarea cuando se resuelva), y devolver el control a quien llamó de inmediato. Conceptualmente es la misma transformación que una librería de corrutinas basada en generadores solía hacer a mano antes de que el lenguaje la incorporara de forma nativa.
async function getUserAsync(id) {const user = await db.query('SELECT * FROM users WHERE id = ?', [id]);const orders = await db.query('SELECT * FROM orders WHERE user_id = ?', [user.id]);return { user: user, orders: orders };}
Lo que ganas no es un mecanismo distinto, sino control de flujo real del lenguaje: try/catch funciona exactamente igual que en código síncrono, porque el motor realmente está desenrollando una pila de llamadas, no enrutándote a través de un argumento de error en un callback o un .catch que podrías olvidar. Lo que puedes perder, si no tienes cuidado, es paralelismo que tenías gratis con promises: hacer await de dos llamadas independientes una tras otra las serializa, mientras que Promise.all las habría corrido de forma concurrente.
#Comparando los tres
| Aspecto | Callbacks | Promises | Async/await |
|---|---|---|---|
| Qué representa "eventualmente" | nada - solo una referencia a función guardada | un objeto con máquina de estados (pending/fulfilled/rejected) | el mismo objeto promise, escondido detrás de sintaxis |
| Composición | anidamiento manual | cadenas .then, Promise.all/allSettled | código lineal; sigue necesitando Promise.all para paralelismo real |
| Manejo de errores | argumento de error primero, hay que revisarlo manualmente cada vez | .catch, se propaga automáticamente por la cadena | try/catch, desenrollado de pila real |
| Bug clásico | pirámide de la perdición, un chequeo de error olvidado | un .then sin retornar, un rechazo no manejado | un await olvidado, o serialización accidental dentro de un bucle |
#Ideas clave
- Los tres se apoyan sobre el mismo event loop de un solo hilo; difieren en sintaxis y garantías de seguridad, no en el modelo de ejecución subyacente.
- Los callbacks de las promises corren como microtareas, que se vacían por completo antes de la siguiente macrotarea (timers, callbacks de I/O) - esto gobierna las preguntas de orden que tanto les gustan a los entrevistadores.
async/awaites azúcar sobre promises: te datry/catchreal y código de apariencia lineal, pero losawaitsecuenciales pueden serializar por accidente trabajo que podría haber corrido en paralelo.- La garantía de orden a nivel de motor - las microtareas siempre se vacían antes de la siguiente macrotarea - vale la pena memorizarla textualmente, ya que es una de las preguntas de "predice la salida" más confiables en entrevistas sobre asincronía.