Programación asíncrona en JavaScript: desde callbacks hasta async/await
JavaScript es un lenguaje de un solo hilo, pero su modelo de ejecución permite procesar eficientemente miles de operaciones simultáneas sin bloquear el hilo principal. Esto es posible gracias a la programación asíncrona. En Node.js, donde las operaciones de entrada/salida (lectura de archivos, consultas a bases de datos, llamadas de red) ocupan el 90% del tiempo, comprender la asincronía se convierte en una habilidad crítica. En esta guía analizaremos las tres etapas evolutivas del trabajo con asincronía: callbacks, promesas y async/await, además de echar un vistazo bajo el capó del Event Loop.
1. Callbacks: el fundamento de la asincronía
Un callback es una función que se pasa a otra función como argumento y se ejecuta después de que se completa una operación asíncrona. Es el patrón más antiguo y básico en JavaScript. En Node.js, muchos módulos incorporados (fs, http) utilizan callbacks.
Ejemplo de lectura de un archivo con callback:
const fs = require('fs');
console.log('Comenzando la lectura del archivo...');
fs.readFile('example.txt', 'utf8', (err, data) => { if (err) { console.error('Error:', err); return; } console.log('Contenido del archivo:', data);});
console.log('El código continúa ejecutándose...');La salida en consola mostrará que "El código continúa ejecutándose..." aparecerá antes que el contenido del archivo. Este es el comportamiento no bloqueante: el hilo principal no espera a que termine la lectura, sino que continúa su ejecución.
El problema de los callbacks: Callback Hell
Con operaciones asíncronas anidadas, el código se convierte en una "pirámide infernal":
fs.readFile('user.json', (err, userData) => { if (err) throw err; const user = JSON.parse(userData); db.query(`SELECT * FROM posts WHERE user_id=${user.id}`, (err, posts) => { if (err) throw err; fs.writeFile('user_posts.json', JSON.stringify(posts), (err) => { if (err) throw err; console.log('¡Listo!'); }); });});Este código es difícil de leer, depurar y mantener. La solución son las promesas.
2. Promesas (Promises): cadenas y manejo de errores
Una Promise es un objeto que representa el resultado futuro de una operación asíncrona. Puede estar en uno de tres estados: pending (pendiente), fulfilled (cumplida exitosamente) o rejected (rechazada con error).
Creación de una promesa simple:
const readFilePromise = (filename) => { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); });};
readFilePromise('example.txt') .then(data => console.log('Datos:', data)) .catch(err => console.error('Error:', err));Cadenas de promesas (Chaining)
La principal ventaja es la posibilidad de encadenar operaciones secuenciales sin anidamiento:
readFilePromise('user.json') .then(userData => { const user = JSON.parse(userData); return dbQueryPromise(`SELECT * FROM posts WHERE user_id=${user.id}`); }) .then(posts => { return writeFilePromise('user_posts.json', JSON.stringify(posts)); }) .then(() => console.log('¡Listo!')) .catch(err => console.error('Error en la cadena:', err));Ejecución paralela: Promise.all
Cuando se necesitan ejecutar varias operaciones independientes simultáneamente:
const promise1 = readFilePromise('file1.txt');const promise2 = readFilePromise('file2.txt');const promise3 = readFilePromise('file3.txt');
Promise.all([promise1, promise2, promise3]) .then(results => { console.log('Todos los archivos leídos:', results); }) .catch(err => console.error('Una de las promesas falló:', err));Promise.all se completa exitosamente solo si todas las promesas se cumplen. Si al menos una falla, se activa