Introducción a la programación asíncrona en JavaScript
JavaScript es un lenguaje de programación de un solo hilo con un modelo de entrada/salida no bloqueante. Esto significa que solo se ejecuta una operación a la vez. Sin embargo, gracias a la programación asíncrona, Node.js puede manejar miles de solicitudes simultáneamente sin bloquear el hilo principal. En este artículo, analizaremos los conceptos básicos de la asincronía: callbacks, Promises y la sintaxis async/await.
¿Qué es la asincronía y por qué es necesaria?
Imagina que pides una pizza. Si te quedas en el mostrador esperando a que la preparen, eso es código síncrono. Todo lo demás se detiene. El enfoque asíncrono: dejas tu pedido y te ocupas de tus asuntos. Cuando la pizza está lista, te llaman. JavaScript funciona exactamente así: no espera a que una operación termine (lectura de archivo, solicitud al servidor), sino que continúa ejecutando el siguiente código. Cuando la operación finaliza, se llama a una función especial: un callback.
En Node.js, la asincronía es especialmente importante porque el servidor debe atender a múltiples clientes simultáneamente. Sin ella, cada nueva solicitud bloquearía el funcionamiento de toda la aplicación.
Código síncrono vs asíncrono: ejemplo
Consideremos un ejemplo simple de lectura de archivo. La versión síncrona bloquea la ejecución:
const fs = require('fs');
// Lectura síncrona — bloquea el hiloconst data = fs.readFileSync('file.txt', 'utf8');console.log('Datos leídos:', data);console.log('Este código se ejecutará solo después de leer el archivo');La versión asíncrona no bloquea:
const fs = require('fs');
// Lectura asíncrona — no bloqueafs.readFile('file.txt', 'utf8', (err, data) => { if (err) throw err; console.log('Datos leídos:', data);});
console.log('Este código se ejecutará inmediatamente, sin esperar la lectura del archivo');Formas principales de trabajar con asincronía
En JavaScript (y Node.js) existen tres enfoques principales para la programación asíncrona. Los veremos desde los más antiguos hasta los modernos.
1. Callback (funciones de retrollamada)
Un callback es una función que se pasa a otra función y se ejecuta después de que se completa una operación asíncrona. Es el método más básico y antiguo.
function getUserData(userId, callback) { setTimeout(() => { const user = { id: userId, name: 'Ana' }; callback(null, user); // primer argumento — error (si existe) }, 1000);}
getUserData(1, (err, user) => { if (err) { console.error('Error:', err); return; } console.log('Usuario:', user.name);});Problema: cuando hay muchas operaciones, el código se convierte en un "infierno de callbacks" (callback hell) — funciones profundamente anidadas que son difíciles de leer y depurar.
2. Promise (Promesas)
Una Promise es un objeto que representa el resultado de una operación asíncrona. Puede estar en uno de tres estados:
- pending (pendiente) — la operación aún no ha terminado
- fulfilled (cumplida) — la operación se completó con éxito
- rejected (rechazada) — la operación terminó con un error
Las promesas resuelven el problema del callback hell y hacen que el código sea más lineal:
function getUserData(userId) { return new Promise((resolve, reject) => { setTimeout(() => { const user = { id: userId, name: 'Iván' }; // resolve — éxito, reject — error resolve(user); // reject(new Error('Usuario no encontrado')); }, 1000); });}
// Uso de la cadena .then()getUserData(1) .then(user => { console.log('Usuario:', user.name); return user.id; }) .then(id => console.log('ID:', id)) .catch(err => console.error('Error:', err)) .finally(() => console.log('Operación completada'));