DE CALLBACKS A ASYNC/AWAIT EN JAVASCRIPT MODERNO
Introducción al manejo asíncrono en JavaScript
JavaScript ejecuta código de forma síncrona por defecto, procesando instrucciones en orden secuencial después de elevar las declaraciones. Las operaciones que involucran tiempos de espera como solicitudes de red o temporizadores no bloquean el hilo principal, permitiendo que el resto del programa continúe mientras se resuelven en segundo plano.
Este comportamiento no bloqueante resulta esencial en aplicaciones web modernas donde la responsividad resulta crítica. Cuando una operación asíncrona finaliza, se invoca una función previamente registrada para procesar el resultado o manejar posibles errores.
console.log("Inicio");
setTimeout(() => {
console.log("Operación completada");
}, 2000);
console.log("Fin");
La salida muestra Inicio seguido inmediatamente por Fin, y dos segundos después aparece Operación completada demostrando el orden no secuencial de ejecución.
Uso de callbacks para operaciones asíncronas
Un callback consiste en una función pasada como argumento a otra que se ejecuta posteriormente cuando ocurre un evento o finaliza una tarea. Este patrón permite manejar resultados de operaciones que toman tiempo sin detener el flujo principal.
Las funciones de solicitud HTTP tradicionales como XMLHttpRequest dependen fuertemente de callbacks para notificar éxito o fracaso.
function solicitar(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = function () {
if (xhr.status === 200) {
callback(null, xhr.responseText);
} else {
callback(new Error("Error en la solicitud"), null);
}
};
xhr.onerror = function () {
callback(new Error("Error de red"), null);
};
xhr.send();
}
Este enfoque funciona adecuadamente para tareas simples sin embargo se complica rápidamente cuando múltiples operaciones dependen unas de otras.
Problemas con anidamiento profundo de callbacks
Cuando varias operaciones asíncronas deben ejecutarse en secuencia el código genera anidamientos excesivos conocidos como callback hell que dificultan la lectura depuración y mantenimiento del programa.
Imagina buscar usuarios en GitHub y luego obtener sus repositorios para cada resultado encontrado. El código se vuelve piramidal rápidamente.
solicitar("https://api.github.com/search/users?q=java", (err, usuarios) => {
if (err) return console.error(err);
const datos = JSON.parse(usuarios).items;
datos.forEach((usuario) => {
solicitar(usuario.repos_url, (err, repos) => {
if (err) return console.error(err);
console.log("Repositorios de", usuario.login, JSON.parse(repos));
});
});
});
Este estilo reduce drásticamente la legibilidad especialmente en flujos complejos con manejo de errores condicionales y bucles.
Introducción de promesas para mejorar el flujo
Las promesas representan un avance significativo al encapsular operaciones asíncronas en objetos con tres estados posibles: pendiente resuelta o rechazada. Una promesa permite encadenar operaciones de forma lineal y centralizar el manejo de errores.
function solicitar(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(xhr.responseText);
} else {
reject(new Error("Error HTTP " + xhr.status));
}
};
xhr.onerror = () => reject(new Error("Fallo de red"));
xhr.send();
});
}
El encadenamiento se realiza mediante then y catch resultando en código mucho más plano.
solicitar("https://api.github.com/search/users?q=java")
.then((respuesta) => {
const items = JSON.parse(respuesta).items;
return Promise.all(items.map((u) => solicitar(u.repos_url)));
})
.then((repositorios) => {
console.log("Todos los repositorios procesados", repositorios);
})
.catch((error) => console.error("Error general", error));
Manejo centralizado de errores facilita la depuración y evita duplicación de código de manejo de fallos.
Combinación de promesas para operaciones paralelas
Promise.all permite ejecutar múltiples promesas concurrentemente y esperar a que todas se resuelvan o rechazar si alguna falla. Esta característica resulta particularmente útil cuando las operaciones no dependen secuencialmente entre sí.
const urls = [
"https://api.example.com/datos1",
"https://api.example.com/datos2",
"https://api.example.com/datos3",
];
Promise.all(urls.map((url) => solicitar(url)))
.then((resultados) => {
console.log("Resultados paralelos", resultados);
})
.catch((err) => console.error("Alguna solicitud falló", err));
En entornos actuales fetch reemplaza XMLHttpRequest ofreciendo una API basada en promesas nativa.
fetch("https://api.github.com/users/octocat")
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => console.error(error));
Generadores y su rol limitado en asincronía
Los generadores permiten pausar y reanudar funciones mediante yield creando iteradores que controlan el flujo. Aunque se exploraron combinaciones con promesas para simular código síncrono su complejidad limita su adopción masiva.
function* generadorEjemplo() {
console.log("Inicio");
yield "pausa 1";
console.log("Continuación");
yield "pausa 2";
}
const iter = generadorEjemplo();
console.log(iter.next().value);
console.log(iter.next().value);
En la práctica generadores asíncronos resultan poco utilizados frente a alternativas más simples y soportadas universalmente.
Async y await para código legible y secuencial
Async/await introducido en ES2017 representa azúcar sintáctica sobre promesas permitiendo escribir código asíncrono con apariencia síncrona. Una función async siempre retorna una promesa y await solo funciona dentro de ellas pausando ejecución hasta resolución.
async function obtenerDatos() {
try {
const respuesta = await fetch("https://api.example.com/data");
const datos = await respuesta.json();
console.log("Datos obtenidos", datos);
return datos;
} catch (error) {
console.error("Fallo en obtención de datos", error);
}
}
obtenerDatos();
Este patrón elimina anidamientos y mejora drásticamente la legibilidad permitiendo usar estructuras de control estándar como if for y try/catch.
Aplicación práctica de async/await en flujos reales
Considera el ejemplo de búsqueda de usuarios y repositorios reescrito con async/await.
async function listarRepositorios() {
try {
const respuestaUsuarios = await fetch(
"https://api.github.com/search/users?q=javascript"
);
const { items } = await respuestaUsuarios.json();
for (const usuario of items) {
const respuestaRepos = await fetch(usuario.repos_url);
const repos = await respuestaRepos.json();
console.log(`Repositorios de ${usuario.login}:`, repos.length);
}
} catch (error) {
console.error("Error procesando repositorios", error);
}
}
listarRepositorios();
Bucles con await dentro ejecutan operaciones secuencialmente lo cual resulta deseable cuando cada paso depende del anterior.
Mejores prácticas actuales con async/await en 2026
En entornos modernos se recomienda combinar async/await con Promise.all para operaciones independientes maximizando rendimiento.
async function procesarMultiples() {
const [datos1, datos2, datos3] = await Promise.all([
fetch("url1").then((r) => r.json()),
fetch("url2").then((r) => r.json()),
fetch("url3").then((r) => r.json()),
]);
console.log("Todos los datos listos", datos1, datos2, datos3);
}
Evita olvidar await ya que genera promesas no resueltas y errores sutiles. Usa nombres descriptivos en funciones async y maneja errores consistentemente con try/catch.
Consideraciones de rendimiento y compatibilidad
Async/await no introduce overhead significativo comparado con promesas puras. En 2026 todos los navegadores y Node.js soportan nativamente esta sintaxis sin necesidad de transpiladores en la mayoría de proyectos.
Para recursos que requieren limpieza asíncrona propuestas como await using (en etapas avanzadas) prometen mejorar el manejo automático de disposables.
Conclusiones
El viaje desde callbacks hasta async/await refleja el esfuerzo continuo por hacer el código asíncrono más intuitivo y mantenible en JavaScript. Los callbacks sirven para casos simples pero generan complejidad rápidamente. Las promesas ofrecen encadenamiento robusto y paralelismo mientras async/await proporciona la sintaxis más limpia y natural para la mayoría de desarrolladores.
En 2026 la combinación de fetch con async/await y Promise.all representa el estándar recomendado para operaciones asíncronas en aplicaciones web y servidores. Dominar estas herramientas permite escribir código eficiente legible y preparado para proyectos escalables manteniendo la responsividad que define la experiencia moderna en tecnología.