Ir al contenido principal

Creación y consumo de promesas

Introducción

Las promesas representan un avance significativo en la forma de manejar operaciones asíncronas en JavaScript. Frente a los callbacks tradicionales que pueden volverse difíciles de gestionar, las promesas ofrecen una estructura más clara y potente para controlar el flujo de ejecución asíncrono. Una promesa es esencialmente un objeto que representa el resultado eventual de una operación asíncrona, permitiéndonos trabajar con ese resultado cuando esté disponible.

En este artículo, exploraremos cómo crear promesas desde cero, cómo consumirlas adecuadamente y las ventajas que ofrecen frente a otros métodos de programación asíncrona. Aprenderemos los fundamentos que nos permitirán construir aplicaciones más robustas y con código más mantenible.

¿Qué es una promesa?

Una promesa en JavaScript es un objeto que representa la eventual finalización (o fallo) de una operación asíncrona y su valor resultante. Podemos pensar en una promesa como un "contenedor" para un valor que puede estar disponible ahora, en el futuro o nunca.

Las promesas tienen tres estados posibles:

  1. Pendiente (pending): Estado inicial, la operación no se ha completado ni rechazado.
  2. Cumplida (fulfilled): La operación se completó con éxito y la promesa tiene un valor resultante.
  3. Rechazada (rejected): La operación falló y la promesa tiene una razón de rechazo.

Creación de promesas

Para crear una promesa, utilizamos el constructor Promise que recibe una función con dos parámetros: resolve y reject.

Sintaxis básica

const miPromesa = new Promise(function(resolve, reject) {
    // Código asíncrono...
    
    // Si la operación es exitosa:
    resolve(valor); // Cambia el estado a "cumplida" con el valor resultante
    
    // Si la operación falla:
    reject(error); // Cambia el estado a "rechazada" con el error resultante
});

Ejemplo simple

Creemos una promesa que se resuelve después de un tiempo determinado:

function esperarSegundos(segundos) {
    return new Promise(function(resolve, reject) {
        // Comprobamos que el parámetro sea válido
        if (typeof segundos !== 'number' || segundos < 0) {
            reject(new Error('El parámetro debe ser un número positivo'));
            return;
        }
        
        console.log(`Esperando ${segundos} segundos...`);
        
        setTimeout(function() {
            // Después del tiempo indicado, resolvemos la promesa
            resolve(`Han pasado ${segundos} segundos`);
        }, segundos * 1000);
    });
}

// La función devuelve una promesa que podemos usar después

En este ejemplo:

  1. Creamos una función que devuelve una promesa
  2. Dentro de la promesa, validamos el parámetro
  3. Si es inválido, rechazamos la promesa inmediatamente
  4. Si es válido, configuramos un temporizador
  5. Cuando el temporizador finaliza, resolvemos la promesa con un mensaje

Consumo de promesas

Una vez creada una promesa, necesitamos formas de acceder a su resultado o manejar sus errores. Para esto, usamos principalmente los métodos .then() y .catch().

El método .then()

El método .then() recibe una o dos funciones como argumentos:

  • La primera función se ejecuta cuando la promesa se resuelve con éxito
  • La segunda función (opcional) se ejecuta cuando la promesa es rechazada
miPromesa.then(
    function(valor) {
        // Se ejecuta cuando la promesa se resuelve
        console.log('Éxito:', valor);
    },
    function(error) {
        // Se ejecuta cuando la promesa es rechazada
        console.error('Error:', error);
    }
);

El método .catch()

El método .catch() es una forma más clara de manejar los errores. Es equivalente a llamar a .then(null, funcionDeError):

miPromesa
    .then(function(valor) {
        console.log('Éxito:', valor);
    })
    .catch(function(error) {
        console.error('Error:', error);
    });

Este patrón es más común y se considera una buena práctica por su claridad.

Ejemplo práctico

Veamos cómo usar la función esperarSegundos que creamos anteriormente:

console.log('Iniciando programa');

esperarSegundos(3)
    .then(function(mensaje) {
        console.log(mensaje); // "Han pasado 3 segundos"
        return esperarSegundos(2); // Devolvemos otra promesa
    })
    .then(function(mensaje) {
        console.log(mensaje); // "Han pasado 2 segundos"
        return 'Operación completada';
    })
    .then(function(mensaje) {
        console.log(mensaje); // "Operación completada"
    })
    .catch(function(error) {
        console.error('Ocurrió un error:', error.message);
    });

console.log('Programa continúa ejecutándose');

La secuencia de ejecución será:

  1. "Iniciando programa"
  2. "Programa continúa ejecutándose"
  3. "Esperando 3 segundos..."
  4. Después de 3 segundos: "Han pasado 3 segundos"
  5. "Esperando 2 segundos..."
  6. Después de 2 segundos: "Han pasado 2 segundos"
  7. "Operación completada"

Conversión de callbacks a promesas

Una práctica común es "promisificar" funciones que utilizan callbacks para trabajar con un estilo más moderno:

// Función tradicional con callbacks
function leerArchivo(ruta, callback) {
    setTimeout(function() {
        if (ruta.includes('noexiste')) {
            callback(new Error('El archivo no existe'));
        } else {
            callback(null, `Contenido del archivo ${ruta}`);
        }
    }, 1000);
}

// Versión promisificada
function leerArchivoPromesa(ruta) {
    return new Promise(function(resolve, reject) {
        leerArchivo(ruta, function(error, contenido) {
            if (error) {
                reject(error);
            } else {
                resolve(contenido);
            }
        });
    });
}

// Uso
leerArchivoPromesa('documento.txt')
    .then(function(contenido) {
        console.log(contenido);
    })
    .catch(function(error) {
        console.error('Error de lectura:', error.message);
    });

Promesas vs. callbacks

Las promesas ofrecen varias ventajas sobre los callbacks tradicionales:

1. Mejor manejo de errores

Con callbacks anidados, el manejo de errores puede volverse complicado:

// Callback hell con manejo de errores
funcionA(function(resultadoA, errorA) {
    if (errorA) {
        manejarError(errorA);
        return;
    }
    
    funcionB(resultadoA, function(resultadoB, errorB) {
        if (errorB) {
            manejarError(errorB);
            return;
        }
        
        funcionC(resultadoB, function(resultadoC, errorC) {
            if (errorC) {
                manejarError(errorC);
                return;
            }
            
            console.log('Resultado final:', resultadoC);
        });
    });
});

Con promesas, esto se vuelve mucho más limpio:

funcionAPromesa()
    .then(funcionBPromesa)
    .then(funcionCPromesa)
    .then(function(resultadoFinal) {
        console.log('Resultado final:', resultadoFinal);
    })
    .catch(function(error) {
        manejarError(error);
    });

2. Composición más sencilla

Las promesas se pueden componer fácilmente, pasando los resultados de una a otra en una cadena:

obtenerUsuario(id)
    .then(function(usuario) {
        return obtenerPermisos(usuario);
    })
    .then(function(permisos) {
        return verificarAcceso(permisos);
    })
    .then(function(tieneAcceso) {
        if (tieneAcceso) {
            console.log('Acceso concedido');
        } else {
            console.log('Acceso denegado');
        }
    });

3. Garantía de ejecución única

Una promesa solo puede resolverse o rechazarse una vez, lo que evita problemas de callbacks llamados múltiples veces:

// Este código siempre es seguro con promesas
miPromesa
    .then(function(valor) {
        console.log(valor);
    });

Métodos estáticos útiles

El objeto Promise proporciona algunos métodos estáticos útiles:

Promise.resolve()

Crea una promesa que se resuelve inmediatamente con el valor proporcionado:

const promesaResuelta = Promise.resolve('Valor inmediato');

promesaResuelta.then(function(valor) {
    console.log(valor); // "Valor inmediato"
});

Promise.reject()

Crea una promesa que se rechaza inmediatamente con el error proporcionado:

const promesaRechazada = Promise.reject(new Error('Algo salió mal'));

promesaRechazada.catch(function(error) {
    console.error(error.message); // "Algo salió mal"
});

Patrones básicos con promesas

Ejecución en paralelo

Si queremos ejecutar varias operaciones asíncronas en paralelo, podemos usar Promise.all():

const promesa1 = esperarSegundos(2).then(() => 'Resultado 1');
const promesa2 = esperarSegundos(1).then(() => 'Resultado 2');
const promesa3 = esperarSegundos(3).then(() => 'Resultado 3');

// Ejecutamos todas las promesas en paralelo
Promise.all([promesa1, promesa2, promesa3])
    .then(function(resultados) {
        // resultados es un array con los valores resueltos de cada promesa
        console.log('Todos completados:', resultados);
        // ["Resultado 1", "Resultado 2", "Resultado 3"]
    })
    .catch(function(error) {
        // Se ejecuta si cualquiera de las promesas es rechazada
        console.error('Una promesa falló:', error);
    });

Promise.all() espera a que todas las promesas se resuelvan, o falla inmediatamente si alguna falla.

Ejecución secuencial

Para ejecutar promesas en secuencia, podemos encadenarlas:

function secuenciaPromesas(array) {
    // Comenzamos con una promesa resuelta
    return array.reduce(function(promesaAnterior, item) {
        // Para cada elemento, esperamos a que la promesa anterior se complete
        return promesaAnterior.then(function() {
            return esperarSegundos(1).then(function() {
                console.log('Procesando:', item);
                return item;
            });
        });
    }, Promise.resolve()); // Comenzamos con una promesa ya resuelta
}

// Uso
secuenciaPromesas([1, 2, 3, 4])
    .then(function() {
        console.log('Secuencia completada');
    })
    .catch(function(error) {
        console.error('Error en la secuencia:', error);
    });

Esto procesará cada elemento uno tras otro, esperando a que termine la operación anterior.

Ejemplo práctico: simulando una API

Veamos un ejemplo práctico donde simulamos una API que realiza operaciones CRUD (Crear, Leer, Actualizar, Eliminar):

// Base de datos simulada (en memoria)
const baseDeDatos = {
    usuarios: [
        { id: 1, nombre: 'Ana', edad: 28 },
        { id: 2, nombre: 'Carlos', edad: 34 },
        { id: 3, nombre: 'Elena', edad: 25 }
    ]
};

// API simulada con promesas
const API = {
    // Obtener todos los usuarios
    obtenerUsuarios: function() {
        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                resolve([...baseDeDatos.usuarios]);
            }, 500);
        });
    },
    
    // Obtener un usuario por ID
    obtenerUsuario: function(id) {
        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                const usuario = baseDeDatos.usuarios.find(u => u.id === id);
                
                if (usuario) {
                    resolve({...usuario});
                } else {
                    reject(new Error(`No existe usuario con ID ${id}`));
                }
            }, 500);
        });
    },
    
    // Crear un nuevo usuario
    crearUsuario: function(datos) {
        return new Promise(function(resolve, reject) {
            setTimeout(function() {
                // Validaciones básicas
                if (!datos.nombre || !datos.edad) {
                    reject(new Error('Faltan datos obligatorios'));
                    return;
                }
                
                // Crear ID basado en el último existente
                const nuevoId = baseDeDatos.usuarios.length > 0 
                    ? baseDeDatos.usuarios[baseDeDatos.usuarios.length - 1].id + 1 
                    : 1;
                
                const nuevoUsuario = {
                    id: nuevoId,
                    nombre: datos.nombre,
                    edad: datos.edad
                };
                
                baseDeDatos.usuarios.push(nuevoUsuario);
                resolve({...nuevoUsuario});
            }, 800);
        });
    }
};

// Uso de nuestra API
console.log('Obteniendo usuarios...');

API.obtenerUsuarios()
    .then(function(usuarios) {
        console.log('Usuarios obtenidos:', usuarios);
        
        // Obtenemos un usuario específico
        return API.obtenerUsuario(2);
    })
    .then(function(usuario) {
        console.log('Usuario encontrado:', usuario);
        
        // Creamos un nuevo usuario
        return API.crearUsuario({ nombre: 'Laura', edad: 31 });
    })
    .then(function(nuevoUsuario) {
        console.log('Usuario creado:', nuevoUsuario);
        
        // Verificamos que se haya añadido a la lista
        return API.obtenerUsuarios();
    })
    .then(function(usuariosActualizados) {
        console.log('Lista actualizada:', usuariosActualizados);
    })
    .catch(function(error) {
        console.error('Error en la operación:', error.message);
    })
    .finally(function() {
        console.log('Operaciones completadas (con o sin éxito)');
    });

Este ejemplo muestra cómo las promesas nos permiten encadenar operaciones asíncronas de manera legible y manejar errores de forma centralizada.

Resumen

Las promesas son una potente herramienta para manejar operaciones asíncronas en JavaScript, ofreciendo una sintaxis más clara y un mejor manejo de errores que los callbacks tradicionales. Hemos aprendido a:

  • Crear promesas utilizando el constructor Promise
  • Consumir promesas con los métodos .then() y .catch()
  • Encadenar promesas para crear secuencias de operaciones
  • Convertir funciones basadas en callbacks a promesas
  • Utilizar métodos útiles como Promise.resolve() y Promise.all()

En el próximo artículo, profundizaremos en los estados de las promesas y cómo gestionarlos adecuadamente, lo que nos permitirá entender mejor su comportamiento interno y aprovechar todo su potencial.