HDP115

Asincronía, Callbacks y Promesas

Aprende a manejar operaciones asíncronas en JavaScript mediante callbacks, promesas y async/await para crear aplicaciones eficientes y responsivas.

CE

Cristian Escalante

Última actualización: 21 de abril de 2025

javascript
programación web
desarrollo frontend

Introducción a la programación asíncrona

La programación asíncrona permite que tu código continúe ejecutándose mientras espera que se completen ciertas operaciones, como solicitudes de red, operaciones de archivo o temporizadores. Esto es fundamental en JavaScript, especialmente en entornos como navegadores y Node.js, donde la naturaleza de un solo hilo podría bloquear la interfaz de usuario o el servidor.

Operaciones síncronas vs. asíncronas

Operaciones síncronas:

  • Se ejecutan secuencialmente, una después de otra
  • Bloquean la ejecución hasta que se completan
  • Son predecibles y fáciles de razonar
// Código síncrono
console.log("Inicio");
const resultado = operacionLenta(); // Bloquea hasta completarse
console.log("Resultado:", resultado);
console.log("Fin");

Operaciones asíncronas:

  • No bloquean la ejecución del programa
  • Permiten que otras operaciones se ejecuten mientras esperan
  • Requieren mecanismos especiales para manejar sus resultados
// Código asíncrono
console.log("Inicio");
operacionLentaAsincrona((resultado) => {
  console.log("Resultado:", resultado);
});
console.log("Fin"); // Se ejecuta antes de obtener el resultado

Ejemplos de operaciones asíncronas

  • Solicitudes de red (AJAX, fetch)
  • Operaciones de archivo (en Node.js)
  • Temporizadores (setTimeout, setInterval)
  • Eventos de usuario (clicks, teclas)
  • Animaciones

El event loop de JavaScript

El event loop (bucle de eventos) es el mecanismo que permite a JavaScript manejar operaciones asíncronas a pesar de ser un lenguaje de un solo hilo.

Componentes principales

  1. Call Stack (Pila de llamadas): Registra la posición actual de ejecución
  2. Web APIs / Node APIs: Proporcionan funcionalidades asíncronas (setTimeout, fetch, etc.)
  3. Callback Queue (Cola de callbacks): Almacena callbacks listos para ejecutarse
  4. Event Loop: Comprueba si la pila está vacía y mueve callbacks de la cola a la pila

Funcionamiento

console.log("1");

setTimeout(() => {
  console.log("2");
}, 0);

Promise.resolve().then(() => {
  console.log("3");
});

console.log("4");

// Salida: 1, 4, 3, 2

Explicación:

  1. console.log("1") se ejecuta inmediatamente
  2. setTimeout se registra, pero su callback va a la cola de tareas
  3. La promesa se resuelve y su callback va a la microtask queue
  4. console.log("4") se ejecuta inmediatamente
  5. La pila se vacía, se ejecutan primero las microtareas (promesa)
  6. Finalmente se ejecuta el callback de setTimeout

Microtareas vs. Macrotareas

  • Microtareas: Promesas, queueMicrotask, MutationObserver
  • Macrotareas: setTimeout, setInterval, eventos de UI, I/O

Las microtareas tienen prioridad sobre las macrotareas y se ejecutan inmediatamente después de que la pila se vacía.

Callbacks

Los callbacks son funciones que se pasan como argumentos a otras funciones y se ejecutan después de que ocurra algún evento o se complete una operación.

Definición y uso básico

function procesarDatos(datos, callback) {
  // Procesar datos
  const resultado = datos.map(x => x * 2);
  
  // Llamar al callback con el resultado
  callback(resultado);
}

procesarDatos([1, 2, 3], (resultado) => {
  console.log("Resultado procesado:", resultado); // [2, 4, 6]
});

Callbacks en funciones asíncronas

function obtenerDatosDeServidor(id, callback) {
  console.log(`Solicitando datos para id: ${id}...`);
  
  // Simular solicitud de red
  setTimeout(() => {
    const datos = { id: id, nombre: "Producto " + id };
    callback(null, datos); // Primer parámetro null indica que no hay error
  }, 2000);
}

obtenerDatosDeServidor(123, (error, datos) => {
  if (error) {
    console.error("Error:", error);
    return;
  }
  console.log("Datos recibidos:", datos);
});

console.log("Continuando ejecución mientras se obtienen los datos...");

Manejo de errores con callbacks

El patrón común en Node.js es pasar el error como primer parámetro:

function dividir(a, b, callback) {
  if (b === 0) {
    callback(new Error("No se puede dividir por cero"));
    return;
  }
  
  setTimeout(() => {
    callback(null, a / b);
  }, 1000);
}

dividir(10, 2, (error, resultado) => {
  if (error) {
    console.error("Error:", error.message);
    return;
  }
  console.log("Resultado:", resultado); // 5
});

dividir(10, 0, (error, resultado) => {
  if (error) {
    console.error("Error:", error.message); // "No se puede dividir por cero"
    return;
  }
  console.log("Resultado:", resultado); // No se ejecuta
});

Callback Hell (Infierno de callbacks)

Cuando se anidan múltiples operaciones asíncronas, el código puede volverse difícil de leer y mantener:

obtenerUsuario(userId, (error, usuario) => {
  if (error) {
    console.error(error);
    return;
  }
  
  obtenerPermisos(usuario.id, (error, permisos) => {
    if (error) {
      console.error(error);
      return;
    }
    
    obtenerPublicaciones(usuario.id, (error, publicaciones) => {
      if (error) {
        console.error(error);
        return;
      }
      
      obtenerComentarios(publicaciones[0].id, (error, comentarios) => {
        if (error) {
          console.error(error);
          return;
        }
        
        // Código cada vez más anidado...
        console.log("Comentarios:", comentarios);
      });
    });
  });
});

Este patrón de código anidado es conocido como "callback hell" o "pirámide de la perdición".

Promesas

Las promesas son objetos que representan la eventual finalización (o fallo) de una operación asíncrona y su valor resultante.

Estados de una promesa

Una promesa puede estar en uno de estos estados:

  1. Pending (pendiente): Estado inicial, ni cumplida ni rechazada
  2. Fulfilled (cumplida): La operación se completó con éxito
  3. Rejected (rechazada): La operación falló
  4. Settled (resuelta): La promesa ha sido cumplida o rechazada

Creación de promesas

const miPromesa = new Promise((resolve, reject) => {
  // Operación asíncrona
  const exito = true;
  
  if (exito) {
    resolve("¡Operación completada!"); // Cumplir la promesa
  } else {
    reject(new Error("Algo salió mal")); // Rechazar la promesa
  }
});

Consumir promesas con .then() y .catch()

miPromesa
  .then(resultado => {
    console.log("Éxito:", resultado);
    return "Valor procesado: " + resultado;
  })
  .then(nuevoResultado => {
    console.log(nuevoResultado);
  })
  .catch(error => {
    console.error("Error:", error.message);
  })
  .finally(() => {
    console.log("Promesa finalizada (exitosa o no)");
  });

Convertir callbacks a promesas

// Función con callback
function obtenerDatosCallback(id, callback) {
  setTimeout(() => {
    if (id <= 0) {
      callback(new Error("ID no válido"));
    } else {
      callback(null, { id, nombre: "Producto " + id });
    }
  }, 1000);
}

// Versión con promesas
function obtenerDatos(id) {
  return new Promise((resolve, reject) => {
    obtenerDatosCallback(id, (error, datos) => {
      if (error) {
        reject(error);
      } else {
        resolve(datos);
      }
    });
  });
}

// Uso
obtenerDatos(123)
  .then(datos => console.log("Datos:", datos))
  .catch(error => console.error("Error:", error.message));

Encadenamiento de promesas

Las promesas permiten encadenar operaciones asíncronas de forma legible:

obtenerUsuario(userId)
  .then(usuario => {
    console.log("Usuario:", usuario);
    return obtenerPermisos(usuario.id);
  })
  .then(permisos => {
    console.log("Permisos:", permisos);
    return obtenerPublicaciones(permisos.userId);
  })
  .then(publicaciones => {
    console.log("Publicaciones:", publicaciones);
    return obtenerComentarios(publicaciones[0].id);
  })
  .then(comentarios => {
    console.log("Comentarios:", comentarios);
  })
  .catch(error => {
    console.error("Error en la cadena:", error);
  });

Métodos de Promise

Promise.all()

Ejecuta múltiples promesas en paralelo y espera a que todas se completen:

const promesa1 = fetch('/api/usuarios');
const promesa2 = fetch('/api/productos');
const promesa3 = fetch('/api/pedidos');

Promise.all([promesa1, promesa2, promesa3])
  .then(([resUsuarios, resProductos, resPedidos]) => {
    // Todas las promesas se completaron exitosamente
    return Promise.all([
      resUsuarios.json(),
      resProductos.json(),
      resPedidos.json()
    ]);
  })
  .then(([usuarios, productos, pedidos]) => {
    console.log({ usuarios, productos, pedidos });
  })
  .catch(error => {
    // Si cualquiera falla, se ejecuta el catch
    console.error("Error en alguna solicitud:", error);
  });

Promise.race()

Devuelve la primera promesa que se resuelva (ya sea cumplida o rechazada):

const promesaRapida = new Promise(resolve => setTimeout(() => resolve("Rápida"), 500));
const promesaLenta = new Promise(resolve => setTimeout(() => resolve("Lenta"), 1000));

Promise.race([promesaRapida, promesaLenta])
  .then(resultado => {
    console.log("La ganadora fue:", resultado); // "Rápida"
  });

// Ejemplo práctico: timeout para una solicitud
function solicitarConTimeout(url, ms) {
  const promesaFetch = fetch(url).then(res => res.json());
  const promesaTimeout = new Promise((_, reject) => 
    setTimeout(() => reject(new Error("Timeout")), ms)
  );
  
  return Promise.race([promesaFetch, promesaTimeout]);
}

solicitarConTimeout('/api/datos', 5000)
  .then(datos => console.log(datos))
  .catch(error => console.error(error.message));

Promise.any() (ES2021)

Devuelve la primera promesa que se cumpla (ignora rechazos):

const promesa1 = new Promise((resolve, reject) => setTimeout(() => reject(new Error("Error 1")), 1000));
const promesa2 = new Promise(resolve => setTimeout(() => resolve("Éxito 2"), 2000));
const promesa3 = new Promise(resolve => setTimeout(() => resolve("Éxito 3"), 3000));

Promise.any([promesa1, promesa2, promesa3])
  .then(resultado => {
    console.log("Primera en tener éxito:", resultado); // "Éxito 2"
  })
  .catch(error => {
    console.error("Todas fallaron:", error);
  });

Promise.allSettled() (ES2020)

Espera a que todas las promesas se resuelvan (cumplidas o rechazadas) y devuelve un array con los resultados:

const promesas = [
  Promise.resolve("éxito 1"),
  Promise.reject(new Error("error")),
  Promise.resolve("éxito 2")
];

Promise.allSettled(promesas)
  .then(resultados => {
    console.log(resultados);
    // [
    //   { status: "fulfilled", value: "éxito 1" },
    //   { status: "rejected", reason: Error: "error" },
    //   { status: "fulfilled", value: "éxito 2" }
    // ]
    
    const exitosos = resultados
      .filter(r => r.status === "fulfilled")
      .map(r => r.value);
    
    console.log("Resultados exitosos:", exitosos);
  });

Async/Await

Async/await es una sintaxis que facilita el trabajo con promesas, haciendo que el código asíncrono se vea y se comporte más como código síncrono.

Funciones async

Una función async siempre devuelve una promesa:

async function obtenerDatos() {
  return "Datos obtenidos"; // Se envuelve automáticamente en Promise.resolve()
}

obtenerDatos().then(console.log); // "Datos obtenidos"

// Equivalente a:
function obtenerDatosPromesa() {
  return Promise.resolve("Datos obtenidos");
}

Palabra clave await

La palabra clave await pausa la ejecución de una función async hasta que una promesa se resuelva:

async function mostrarUsuario(id) {
  try {
    console.log("Obteniendo usuario...");
    const usuario = await obtenerUsuario(id); // Espera a que la promesa se resuelva
    
    console.log("Obteniendo permisos...");
    const permisos = await obtenerPermisos(usuario.id);
    
    console.log("Usuario:", usuario);
    console.log("Permisos:", permisos);
    
    return { usuario, permisos };
  } catch (error) {
    console.error("Error:", error);
    throw error; // Propagar el error
  }
}

// Llamar a la función async
mostrarUsuario(123)
  .then(resultado => console.log("Todo completado:", resultado))
  .catch(error => console.error("Error general:", error));

Ejecución paralela con async/await

async function obtenerDatosParalelo() {
  try {
    // Iniciar ambas solicitudes en paralelo
    const promesaUsuarios = obtenerUsuarios();
    const promesaProductos = obtenerProductos();
    
    // Esperar a que ambas terminen
    const usuarios = await promesaUsuarios;
    const productos = await promesaProductos;
    
    return { usuarios, productos };
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

// Alternativa usando Promise.all
async function obtenerDatosParaleloAll() {
  try {
    const [usuarios, productos] = await Promise.all([
      obtenerUsuarios(),
      obtenerProductos()
    ]);
    
    return { usuarios, productos };
  } catch (error) {
    console.error("Error:", error);
    throw error;
  }
}

Bucles con async/await

// Secuencial (cada iteración espera a la anterior)
async function procesarSecuencial(items) {
  const resultados = [];
  
  for (const item of items) {
    const resultado = await procesarItem(item); // Espera antes de la siguiente iteración
    resultados.push(resultado);
  }
  
  return resultados;
}

// Paralelo (todas las promesas se inician a la vez)
async function procesarParalelo(items) {
  const promesas = items.map(item => procesarItem(item));
  return Promise.all(promesas);
}

Manejo de errores en código asíncrono

Try/catch con async/await

async function obtenerDatosUsuario(id) {
  try {
    const usuario = await obtenerUsuario(id);
    const publicaciones = await obtenerPublicaciones(usuario.id);
    return { usuario, publicaciones };
  } catch (error) {
    console.error("Error al obtener datos:", error);
    // Puedes manejar diferentes tipos de errores
    if (error.name === "NotFoundError") {
      return { error: "Usuario no encontrado" };
    }
    throw new Error("No se pudieron obtener los datos: " + error.message);
  } finally {
    console.log("Operación finalizada");
  }
}

Errores en promesas encadenadas

obtenerUsuario(id)
  .then(usuario => {
    if (!usuario.activo) {
      throw new Error("Usuario inactivo");
    }
    return obtenerPermisos(usuario.id);
  })
  .then(permisos => {
    if (permisos.nivel < 5) {
      throw new Error("Permisos insuficientes");
    }
    return obtenerDatos(permisos.nivel);
  })
  .catch(error => {
    // Maneja todos los errores de la cadena
    console.error("Error en el proceso:", error.message);
    
    // Puedes determinar dónde ocurrió el error
    if (error.message === "Usuario inactivo") {
      // Manejar específicamente
    }
  });

Errores no capturados

Las promesas rechazadas no capturadas pueden causar problemas:

// Escuchar errores no capturados en el navegador
window.addEventListener('unhandledrejection', event => {
  console.error('Promesa rechazada no capturada:', event.promise, 'Razón:', event.reason);
  event.preventDefault(); // Prevenir el comportamiento predeterminado
});

// En Node.js
process.on('unhandledRejection', (reason, promise) => {
  console.error('Promesa rechazada no capturada:', promise, 'Razón:', reason);
});

Patrones y mejores prácticas

Promisificación

Convertir APIs basadas en callbacks a promesas:

// Función de utilidad para promisificar
function promisify(fn) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (error, result) => {
        if (error) {
          reject(error);
        } else {
          resolve(result);
        }
      });
    });
  };
}

// Ejemplo de uso
const fs = require('fs');
const readFile = promisify(fs.readFile);

// Ahora puedes usar async/await
async function leerArchivo() {
  try {
    const contenido = await readFile('archivo.txt', 'utf8');
    console.log(contenido);
  } catch (error) {
    console.error('Error al leer archivo:', error);
  }
}

Manejo de concurrencia

async function procesarLotes(items, tamañoLote = 5) {
  const resultados = [];
  
  // Procesar en lotes para controlar la concurrencia
  for (let i = 0; i < items.length; i += tamañoLote) {
    const lote = items.slice(i, i + tamañoLote);
    const resultadosLote = await Promise.all(
      lote.map(item => procesarItem(item))
    );
    resultados.push(...resultadosLote);
    
    // Opcional: esperar un poco entre lotes
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  
  return resultados;
}

Cancelación de promesas

JavaScript no tiene un mecanismo nativo para cancelar promesas, pero puedes implementarlo:

function obtenerDatosCancelable() {
  let cancelado = false;
  
  const promesa = new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      if (!cancelado) {
        resolve({ datos: "Información obtenida" });
      }
    }, 5000);
    
    // Función para limpiar si se cancela
    promesa.cancel = () => {
      cancelado = true;
      clearTimeout(id);
      reject(new Error("Operación cancelada"));
    };
  });
  
  return promesa;
}

// Uso
const peticion = obtenerDatosCancelable();
peticion.then(console.log).catch(console.error);

// Para cancelar:
setTimeout(() => {
  peticion.cancel();
}, 2000);

AbortController (API moderna)

async function obtenerDatosConTimeout(url, tiempoLimite) {
  const controller = new AbortController();
  const { signal } = controller;
  
  // Configurar timeout
  const timeoutId = setTimeout(() => controller.abort(), tiempoLimite);
  
  try {
    const respuesta = await fetch(url, { signal });
    clearTimeout(timeoutId); // Limpiar timeout si la solicitud tiene éxito
    return await respuesta.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      throw new Error(`La solicitud excedió el tiempo límite de ${tiempoLimite}ms`);
    }
    throw error;
  }
}

// Uso
obtenerDatosConTimeout('https://api.ejemplo.com/datos', 3000)
  .then(datos => console.log('Datos:', datos))
  .catch(error => console.error('Error:', error.message));

Resumen

  • Callbacks: Funciones pasadas como argumentos que se ejecutan después de completar una operación
  • Promesas: Objetos que representan un valor futuro, con estados (pendiente, cumplida, rechazada)
  • Async/await: Sintaxis que facilita el trabajo con promesas, haciendo el código asíncrono más legible
  • Métodos de Promise: Promise.all(), Promise.race(), Promise.any(), Promise.allSettled()
  • Manejo de errores: try/catch con async/await, .catch() con promesas

La programación asíncrona es fundamental en JavaScript para crear aplicaciones responsivas y eficientes. Dominar estos conceptos te permitirá manejar operaciones como solicitudes de red, operaciones de E/S y temporizadores de manera efectiva.

DOM y Eventos
Aprende a interactuar con páginas web mediante JavaScript, m...
Fetch API y AJAX
Aprende a realizar peticiones HTTP desde JavaScript para com...

Conceptos Básicos de HTML

Aprende los conceptos básicos de HTML

Conceptos Básicos de CSS

Aprende los conceptos básicos de CSS

Conceptos Básicos SQL

Aprende los conceptos básicos de SQL

Conceptos Básicos de GIT

Aprende los conceptos básicos de GIT

Conceptos Básicos de Python

Aprende los conceptos básicos de Python

Conceptos Básicos de UML

Aprende los conceptos básicos de UML

Refuerzo Academico de Herramientas de Productividad 2025