Compartir en Twitter
Go to Homepage

ENTENDIENDO PROMESAS EN JAVASCRIPT: GUÍA COMPLETA 2025

November 2, 2025

Introducción a las Promesas en JavaScript

Las promesas representan un pilar fundamental en el manejo de operaciones asíncronas en JavaScript. En un entorno donde las aplicaciones web modernas requieren respuestas rápidas y eficientes, comprender cómo funcionan las promesas permite a los desarrolladores escribir código más limpio, mantenible y robusto. Este tutorial explora en profundidad qué son las promesas, cómo se crean, cómo se consumen y cómo resuelven problemas comunes en el desarrollo frontend y backend.

Una promesa es un objeto que actúa como un contenedor para el resultado futuro de una operación asíncrona. En lugar de bloquear la ejecución del programa mientras se espera una respuesta (como una petición a un servidor), la promesa permite que el código continúe ejecutándose y proporciona mecanismos para manejar el resultado cuando esté disponible.

const miPromesa = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve("Operación completada con éxito");
    }, 2000);
});

Este ejemplo muestra la estructura básica de una promesa. El constructor recibe una función con dos parámetros: resolve y reject. Estos son callbacks que se invocan cuando la operación termina exitosamente o con error.

Estructura y Creación de Promesas

Crear una promesa requiere usar el constructor Promise con la palabra clave new. La función ejecutora recibe dos argumentos que son funciones: una para indicar éxito y otra para señalar fracaso.

function obtenerDatosUsuario(id) {
    return new Promise((resolve, reject) => {
        if (id > 0) {
            const usuario = {
                id: id,
                nombre: "Ana García",
                email: "[email protected]",
            };
            resolve(usuario);
        } else {
            reject(new Error("ID de usuario inválido"));
        }
    });
}

La creación de promesas sigue un patrón consistente. Las promesas encapsulan operaciones que pueden tardar tiempo en completarse, como llamadas a APIs, lectura de archivos o consultas a bases de datos.

Estados de una Promesa

Toda promesa pasa por tres estados posibles durante su ciclo de vida:

  • Pending: Estado inicial cuando se crea la promesa
  • Fulfilled: La operación se completó exitosamente
  • Rejected: La operación falló por algún motivo
console.log(miPromesa); // Promise { <pending> }

miPromesa.then((resultado) => {
    console.log(miPromesa); // Promise { <fulfilled> }
});

Estos estados son inmutables. Una vez que una promesa pasa a fulfilled o rejected, no puede cambiar de estado nuevamente. Esta característica garantiza predictibilidad en el flujo del programa.

Consumo de Promesas con then y catch

El consumo de promesas se realiza principalmente mediante los métodos then y catch. El método then se ejecuta cuando la promesa se resuelve exitosamente, mientras que catch maneja los errores.

obtenerDatosUsuario(123)
    .then((usuario) => {
        console.log(`Usuario: ${usuario.nombre}`);
        return procesarDatos(usuario);
    })
    .then((datosProcesados) => {
        console.log("Datos procesados:", datosProcesados);
    })
    .catch((error) => {
        console.error("Error:", error.message);
    });

La cadena de then permite encadenar múltiples operaciones asíncronas de forma secuencial y legible. Cada then recibe el valor retornado por el then anterior.

Encadenamiento de Promesas

El encadenamiento es una de las ventajas más poderosas de las promesas. Permite ejecutar operaciones asíncronas en secuencia sin caer en la complejidad de callbacks anidados.

function paso1() {
    return new Promise((resolve) => {
        setTimeout(() => resolve("Paso 1 completado"), 1000);
    });
}

function paso2(datos) {
    return new Promise((resolve) => {
        setTimeout(() => resolve(`${datos} -> Paso 2`), 1000);
    });
}

paso1()
    .then((resultado1) => paso2(resultado1))
    .then((resultado2) => paso3(resultado2))
    .then((resultadoFinal) => console.log(resultadoFinal))
    .catch((error) => console.error(error));

Este patrón evita el conocido “callback hell” que ocurre cuando se anidan múltiples funciones de callback.

Manejo de Errores en Promesas

El manejo de errores en promesas es más elegante que con callbacks tradicionales. Un solo catch al final de la cadena captura cualquier error que ocurra en cualquier punto de la secuencia.

function operacionCritica() {
    return new Promise((resolve, reject) => {
        const exito = Math.random() > 0.5;
        setTimeout(() => {
            if (exito) {
                resolve("Operación exitosa");
            } else {
                reject(new Error("Falló la operación crítica"));
            }
        }, 1500);
    });
}

operacionCritica()
    .then((resultado) => {
        console.log(resultado);
        return otraOperacion();
    })
    .catch((error) => {
        console.error("Error capturado:", error.message);
        // Recuperación de error
        return "Valor por defecto";
    })
    .then((valorRecuperado) => {
        console.log("Continúa con:", valorRecuperado);
    });

El error “burbujea” hacia abajo hasta encontrar el primer catch en la cadena.

Promesas y el Event Loop

JavaScript es single-threaded, lo que significa que solo puede ejecutar una tarea a la vez. Las promesas trabajan en conjunto con el event loop para manejar operaciones asíncronas sin bloquear la ejecución.

console.log("Inicio");

setTimeout(() => console.log("Timeout"), 0);

Promise.resolve()
    .then(() => console.log("Promesa"))
    .then(() => console.log("Promesa 2"));

console.log("Fin");

// Salida:
// Inicio
// Fin
// Promesa
// Promesa 2
// Timeout

Las promesas tienen prioridad sobre los callbacks de setTimeout en la cola de microtasks.

Creación de Promesas Personalizadas

Crear promesas personalizadas permite encapsular lógica compleja de forma reutilizable.

function leerArchivo(nombre) {
    return new Promise((resolve, reject) => {
        const contenido = simularLecturaArchivo(nombre);
        if (contenido) {
            resolve({
                nombre: nombre,
                contenido: contenido,
                timestamp: Date.now(),
            });
        } else {
            reject(new Error(`Archivo ${nombre} no encontrado`));
        }
    });
}

// Uso
leerArchivo("config.json")
    .then((datos) => console.log("Archivo leído:", datos))
    .catch((err) => console.error(err.message));

Promesas vs Callbacks

Las promesas surgieron para resolver los problemas de los callbacks, especialmente el callback hell.

// Con callbacks (problema)
getUsuario(id, (err, usuario) => {
    if (err) return handleError(err);
    getPermisos(usuario.id, (err, permisos) => {
        if (err) return handleError(err);
        getProyectos(permisos, (err, proyectos) => {
            if (err) return handleError(err);
            renderizar(proyectos);
        });
    });
});

// Con promesas (solución)
getUsuario(id)
    .then((usuario) => getPermisos(usuario.id))
    .then((permisos) => getProyectos(permisos))
    .then((proyectos) => renderizar(proyectos))
    .catch(handleError);

La versión con promesas es más legible y mantenible.

Ejemplo Práctico: API de Temperatura

Un ejemplo clásico para entender promesas es simular una consulta a un supercomputador que mide la temperatura de una sopa.

function medirTemperaturaSopa() {
    return new Promise((resolve, reject) => {
        const temperatura = Math.floor(Math.random() * 300) + 1;
        const retraso = Math.floor(Math.random() * 9000) + 1000;

        setTimeout(() => {
            if (temperatura >= 60 && temperatura <= 80) {
                resolve({
                    estado: "Just Right",
                    temperatura: temperatura,
                    retraso: retraso,
                });
            } else if (temperatura < 60) {
                reject({
                    estado: "Too Cold",
                    temperatura: temperatura,
                    retraso: retraso,
                });
            } else {
                reject({
                    estado: "Too Hot",
                    temperatura: temperatura,
                    retraso: retraso,
                });
            }
        }, retraso);
    });
}

// Consumo de la promesa
medirTemperaturaSopa()
    .then((resultado) => {
        console.log(`¡Perfecto! Temperatura: ${resultado.temperatura}°C`);
    })
    .catch((error) => {
        console.log(`No es ideal: ${error.estado}, ${error.temperatura}°C`);
    });

Este ejemplo muestra cómo las promesas manejan tanto el éxito como el fracaso de forma estructurada.

Promesas en APIs Modernas

Las APIs modernas del navegador y Node.js retornan promesas nativamente.

// Fetch API
fetch("https://api.ejemplo.com/datos")
    .then((response) => {
        if (!response.ok) throw new Error("Error de red");
        return response.json();
    })
    .then((datos) => console.log(datos))
    .catch((error) => console.error("Error:", error));

// Async/await (sintaxis más moderna)
async function obtenerDatos() {
    try {
        const response = await fetch("https://api.ejemplo.com/datos");
        const datos = await response.json();
        console.log(datos);
    } catch (error) {
        console.error("Error:", error);
    }
}

Aunque async/await es azúcar sintáctico sobre promesas, entender las promesas es fundamental.

Buenas Prácticas con Promesas

  1. Siempre retornar promesas en funciones asíncronas
  2. Manejar siempre los errores con catch
  3. No anidar then innecesariamente
  4. Usar valores de retorno en then para encadenar
  5. Evitar crear promesas cuando ya existen métodos que las retornan
// Mal
function procesarDatos(datos) {
    return new Promise((resolve) => {
        const resultado = datos.map((d) => d * 2);
        resolve(resultado);
    });
}

// Bien (si es síncrono)
function procesarDatos(datos) {
    return Promise.resolve(datos.map((d) => d * 2));
}

Errores Comunes

  • Olvidar retornar la promesa en un then
  • No manejar errores adecuadamente
  • Crear promesas innecesarias
  • Mezclar promesas con callbacks sin cuidado
// Error común
getDatos()
    .then((datos) => {
        procesarDatos(datos); // ¡Olvidó return!
    })
    .then((resultado) => {
        // resultado es undefined
    });

// Correcto
getDatos()
    .then((datos) => {
        return procesarDatos(datos); // ¡Importante el return!
    })
    .then((resultado) => {
        console.log(resultado);
    });

Promesas y Performance

Las promesas son eficientes y no bloquean el thread principal. Sin embargo, crear muchas promesas en paralelo puede impactar el rendimiento si no se manejan adecuadamente.

// Procesamiento secuencial (lento)
const promesas = ids.map((id) => obtenerUsuario(id));
promesas.reduce((p, promesa) => p.then(() => promesa), Promise.resolve());

// Procesamiento en paralelo (rápido)
Promise.all(ids.map((id) => obtenerUsuario(id))).then((usuarios) =>
    console.log(usuarios)
);

Promise.all y Promise.race

Métodos estáticos de Promise para manejar múltiples promesas:

// Promise.all - todas deben resolverse
Promise.all([obtenerUsuario(1), obtenerUsuario(2), obtenerUsuario(3)])
    .then((usuarios) => console.log("Todos los usuarios:", usuarios))
    .catch((error) => console.error("Uno falló:", error));

// Promise.race - la primera que se resuelva
Promise.race([peticionServidor1(), peticionServidor2(), timeout(5000)]).then(
    (respuesta) => console.log("Respuesta más rápida:", respuesta)
);

Creación de Utilidades con Promesas

// Timeout como promesa
function timeout(ms) {
    return new Promise((_, reject) => {
        setTimeout(() => reject(new Error("Timeout")), ms);
    });
}

// Retry con promesas
function retry(operacion, intentos = 3) {
    return operacion().catch((error) => {
        if (intentos <= 1) throw error;
        return retry(operacion, intentos - 1);
    });
}

Migración de Callbacks a Promesas

Función utilitaria para convertir callbacks a promesas:

function promisify(fn) {
    return function (...args) {
        return new Promise((resolve, reject) => {
            fn(...args, (error, result) => {
                if (error) reject(error);
                else resolve(result);
            });
        });
    };
}

// Uso
const leerArchivoPromesa = promisify(fs.readFile);
leerArchivoPromesa("archivo.txt", "utf8").then((contenido) =>
    console.log(contenido)
);

Testing con Promesas

test("obtenerUsuario retorna usuario válido", () => {
    return obtenerUsuario(1).then((usuario) => {
        expect(usuario.id).toBe(1);
        expect(usuario.nombre).toBeDefined();
    });
});

// O con async/await
test("obtenerUsuario con async", async () => {
    const usuario = await obtenerUsuario(1);
    expect(usuario).toHaveProperty("email");
});

Patrones Avanzados

// Cache con promesas
const cache = new Map();

function obtenerConCache(clave, fn) {
    if (cache.has(clave)) {
        return cache.get(clave);
    }

    const promesa = fn().then((resultado) => {
        cache.set(clave, Promise.resolve(resultado));
        return resultado;
    });

    cache.set(clave, promesa);
    return promesa;
}

Conclusiones

Las promesas representan una evolución significativa en el manejo de código asíncrono en JavaScript. Las promesas transforman operaciones complejas en flujos de control legibles y mantenibles. Su integración con el event loop, el encadenamiento fluido y el manejo estructurado de errores las convierten en una herramienta esencial para el desarrollo moderno.

Dominar las promesas abre la puerta a patrones más avanzados como async/await, programación reactiva y arquitecturas escalables. La práctica constante con ejemplos reales consolida el entendimiento de sus estados, métodos y mejores prácticas.

En el ecosistema actual de JavaScript, donde las aplicaciones requieren alta responsividad y manejo eficiente de múltiples operaciones concurrentes, las promesas siguen siendo relevantes y fundamentales. Su comprensión profunda permite escribir código más robusto, testable y performant, características esenciales en el desarrollo profesional de aplicaciones web modernas.