Node.js

Node

Tutorial Node: Promises

Aprende a manejar Promises en Node.js con then(), catch() y cómo crear Promises personalizadas para operaciones asíncronas robustas.

Aprende Node y certifícate

then() y catch()

Los métodos then() y catch() constituyen la interfaz fundamental para trabajar con Promises en Node.js. Estos métodos permiten definir qué debe ocurrir cuando una operación asíncrona se completa exitosamente o cuando falla, proporcionando un mecanismo elegante para manejar el flujo de ejecución asíncrono.

Método then()

El método then() se ejecuta cuando una Promise se resuelve correctamente. Este método acepta hasta dos parámetros: una función para manejar el caso de éxito y opcionalmente otra para manejar el caso de error.

const fs = require('fs').promises;

// Leer un archivo usando Promises
fs.readFile('datos.txt', 'utf8')
  .then(contenido => {
    console.log('Archivo leído exitosamente:');
    console.log(contenido);
  });

El valor de retorno de then() es siempre una nueva Promise, lo que permite encadenar múltiples operaciones asíncronas de forma secuencial:

const fs = require('fs').promises;

fs.readFile('entrada.txt', 'utf8')
  .then(contenido => {
    // Procesar el contenido
    const contenidoProcesado = contenido.toUpperCase();
    return contenidoProcesado;
  })
  .then(contenidoFinal => {
    // Escribir el resultado procesado
    return fs.writeFile('salida.txt', contenidoFinal);
  })
  .then(() => {
    console.log('Archivo procesado y guardado correctamente');
  });

Método catch()

El método catch() maneja los errores que pueden ocurrir durante la ejecución de una Promise. Se ejecuta cuando la Promise es rechazada o cuando se produce una excepción en cualquier punto de la cadena:

const fs = require('fs').promises;

fs.readFile('archivo_inexistente.txt', 'utf8')
  .then(contenido => {
    console.log(contenido);
  })
  .catch(error => {
    console.error('Error al leer el archivo:', error.message);
  });

Es importante entender que catch() captura errores de toda la cadena anterior de then(). Si cualquier operación en la cadena falla, la ejecución salta directamente al catch() más cercano:

const fs = require('fs').promises;

fs.readFile('datos.txt', 'utf8')
  .then(contenido => {
    // Si este procesamiento falla, se ejecutará el catch()
    const datos = JSON.parse(contenido);
    return datos;
  })
  .then(datos => {
    // Esta operación también puede fallar
    return fs.writeFile('resultado.json', JSON.stringify(datos, null, 2));
  })
  .catch(error => {
    // Maneja errores de cualquier punto de la cadena
    console.error('Error en el procesamiento:', error.message);
  });

Encadenamiento y propagación de errores

Una característica fundamental del encadenamiento de Promises es que los errores se propagan automáticamente hacia abajo en la cadena hasta encontrar un catch(). Esto permite manejar múltiples operaciones con un solo manejador de errores:

const https = require('https');

function realizarPeticion(url) {
  return new Promise((resolve, reject) => {
    https.get(url, (res) => {
      let datos = '';
      
      res.on('data', chunk => {
        datos += chunk;
      });
      
      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(datos);
        } else {
          reject(new Error(`HTTP ${res.statusCode}`));
        }
      });
    }).on('error', reject);
  });
}

realizarPeticion('https://api.ejemplo.com/datos')
  .then(respuesta => {
    return JSON.parse(respuesta);
  })
  .then(datos => {
    console.log('Datos recibidos:', datos.length, 'elementos');
    return datos.filter(item => item.activo);
  })
  .then(datosActivos => {
    console.log('Elementos activos:', datosActivos.length);
  })
  .catch(error => {
    // Maneja errores de la petición HTTP, parsing JSON o filtrado
    console.error('Error en la operación:', error.message);
  });

Manejo granular de errores

Aunque catch() al final de la cadena es útil para manejo general, también puedes insertar manejadores de error específicos en puntos concretos de la cadena:

const fs = require('fs').promises;

fs.readFile('configuracion.json', 'utf8')
  .catch(error => {
    // Manejo específico para archivo de configuración faltante
    console.log('Usando configuración por defecto');
    return '{"puerto": 3000, "debug": false}';
  })
  .then(contenido => {
    const config = JSON.parse(contenido);
    console.log('Configuración cargada:', config);
    return config;
  })
  .then(config => {
    // Usar la configuración para inicializar la aplicación
    console.log(`Servidor iniciando en puerto ${config.puerto}`);
  })
  .catch(error => {
    // Manejo de errores de parsing o inicialización
    console.error('Error fatal:', error.message);
  });

Transformación de errores

Los métodos then() y catch() también permiten transformar errores para proporcionar información más útil o recuperarse de fallos específicos:

const fs = require('fs').promises;

function leerArchivoConRespaldo(archivo, archivoRespaldo) {
  return fs.readFile(archivo, 'utf8')
    .catch(error => {
      if (error.code === 'ENOENT') {
        console.log(`Archivo ${archivo} no encontrado, usando respaldo`);
        return fs.readFile(archivoRespaldo, 'utf8');
      }
      // Re-lanzar otros tipos de error
      throw error;
    });
}

leerArchivoConRespaldo('datos.txt', 'datos_respaldo.txt')
  .then(contenido => {
    console.log('Contenido leído:', contenido.substring(0, 100));
  })
  .catch(error => {
    console.error('No se pudo leer ningún archivo:', error.message);
  });

Esta aproximación con then() y catch() proporciona un control preciso sobre el flujo asíncrono, permitiendo crear aplicaciones robustas que manejan tanto los casos de éxito como los escenarios de error de manera elegante y predecible.

Creación de Promises

Aunque Node.js proporciona muchas APIs que ya devuelven Promises, en ocasiones necesitarás crear tus propias Promises para encapsular operaciones asíncronas personalizadas o para convertir APIs basadas en callbacks al patrón de Promises.

Constructor de Promise

Una Promise se crea utilizando el constructor Promise, que acepta una función ejecutora como parámetro. Esta función recibe dos argumentos: resolve y reject, que son funciones para indicar el resultado de la operación asíncrona:

const miPromise = new Promise((resolve, reject) => {
  // Lógica asíncrona aquí
  const exito = true;
  
  if (exito) {
    resolve('Operación completada exitosamente');
  } else {
    reject(new Error('La operación falló'));
  }
});

La función ejecutora se ejecuta inmediatamente cuando se crea la Promise. Es importante entender que solo debes llamar a resolve() o reject() una vez, ya que una Promise solo puede cambiar de estado una vez.

Promisificación de callbacks

Una aplicación común de la creación de Promises es convertir funciones basadas en callbacks al patrón de Promises. Node.js incluye muchas APIs que siguen el patrón de callback con error como primer parámetro:

const fs = require('fs');

function leerArchivo(ruta) {
  return new Promise((resolve, reject) => {
    fs.readFile(ruta, 'utf8', (error, datos) => {
      if (error) {
        reject(error);
      } else {
        resolve(datos);
      }
    });
  });
}

// Uso de la función promisificada
leerArchivo('ejemplo.txt')
  .then(contenido => {
    console.log('Contenido del archivo:', contenido);
  })
  .catch(error => {
    console.error('Error al leer:', error.message);
  });

Operaciones asíncronas personalizadas

Las Promises son especialmente útiles para encapsular operaciones asíncronas complejas que involucran múltiples pasos o condiciones:

function procesarDatos(datos, tiempoEspera) {
  return new Promise((resolve, reject) => {
    // Validar entrada
    if (!datos || datos.length === 0) {
      reject(new Error('Los datos no pueden estar vacíos'));
      return;
    }
    
    // Simular procesamiento asíncrono
    setTimeout(() => {
      try {
        const resultado = datos.map(item => ({
          ...item,
          procesado: true,
          timestamp: Date.now()
        }));
        
        resolve(resultado);
      } catch (error) {
        reject(new Error(`Error en procesamiento: ${error.message}`));
      }
    }, tiempoEspera);
  });
}

// Uso de la Promise personalizada
const datosOriginales = [
  { id: 1, nombre: 'Usuario 1' },
  { id: 2, nombre: 'Usuario 2' }
];

procesarDatos(datosOriginales, 1000)
  .then(datosProcesados => {
    console.log('Datos procesados:', datosProcesados);
  })
  .catch(error => {
    console.error('Error:', error.message);
  });

Promises con operaciones de red

Un caso práctico común es crear Promises para operaciones de red que no están disponibles de forma nativa como Promises:

const http = require('http');

function realizarPeticionHTTP(url, opciones = {}) {
  return new Promise((resolve, reject) => {
    const peticion = http.request(url, opciones, (respuesta) => {
      let datos = '';
      
      // Acumular datos de la respuesta
      respuesta.on('data', chunk => {
        datos += chunk;
      });
      
      // Procesar respuesta completa
      respuesta.on('end', () => {
        if (respuesta.statusCode >= 200 && respuesta.statusCode < 300) {
          resolve({
            statusCode: respuesta.statusCode,
            headers: respuesta.headers,
            body: datos
          });
        } else {
          reject(new Error(`HTTP ${respuesta.statusCode}: ${datos}`));
        }
      });
    });
    
    // Manejar errores de conexión
    peticion.on('error', (error) => {
      reject(new Error(`Error de conexión: ${error.message}`));
    });
    
    // Configurar timeout
    peticion.setTimeout(5000, () => {
      peticion.destroy();
      reject(new Error('Timeout: La petición tardó demasiado'));
    });
    
    peticion.end();
  });
}

// Uso de la Promise de red
realizarPeticionHTTP('http://httpbin.org/json')
  .then(respuesta => {
    console.log('Status:', respuesta.statusCode);
    console.log('Datos:', JSON.parse(respuesta.body));
  })
  .catch(error => {
    console.error('Error en petición:', error.message);
  });

Promises con validación y transformación

Las Promises personalizadas pueden incluir lógica de validación y transformación compleja antes de resolver o rechazar:

function validarYProcesarUsuario(datosUsuario) {
  return new Promise((resolve, reject) => {
    // Validaciones síncronas
    if (!datosUsuario.email || !datosUsuario.email.includes('@')) {
      reject(new Error('Email inválido'));
      return;
    }
    
    if (!datosUsuario.nombre || datosUsuario.nombre.length < 2) {
      reject(new Error('Nombre debe tener al menos 2 caracteres'));
      return;
    }
    
    // Simulación de validación asíncrona (ej: verificar email único)
    setTimeout(() => {
      const emailExiste = Math.random() < 0.3; // 30% probabilidad
      
      if (emailExiste) {
        reject(new Error('El email ya está registrado'));
      } else {
        // Procesar y normalizar datos
        const usuarioProcesado = {
          id: Date.now(),
          nombre: datosUsuario.nombre.trim(),
          email: datosUsuario.email.toLowerCase(),
          fechaRegistro: new Date().toISOString(),
          activo: true
        };
        
        resolve(usuarioProcesado);
      }
    }, 500);
  });
}

// Uso con manejo de diferentes tipos de error
const nuevoUsuario = {
  nombre: 'Juan Pérez',
  email: 'juan@ejemplo.com'
};

validarYProcesarUsuario(nuevoUsuario)
  .then(usuario => {
    console.log('Usuario creado:', usuario);
  })
  .catch(error => {
    console.error('Error de validación:', error.message);
  });

Buenas prácticas en la creación

Al crear Promises personalizadas, es importante seguir ciertas mejores prácticas:

  • Maneja siempre los errores: Asegúrate de que cualquier error posible resulte en una llamada a reject().
function operacionSegura() {
  return new Promise((resolve, reject) => {
    try {
      // Operación que puede fallar
      const resultado = JSON.parse(datosJSON);
      resolve(resultado);
    } catch (error) {
      reject(error);
    }
  });
}
  • No mezcles callbacks y Promises: Si una función devuelve una Promise, no uses también callbacks.
// Incorrecto: mezclar patrones
function malaFuncion(callback) {
  return new Promise((resolve, reject) => {
    // Confuso: ¿callback o Promise?
  });
}

// Correcto: solo Promise
function buenaFuncion() {
  return new Promise((resolve, reject) => {
    // Solo lógica de Promise
  });
}
  • Resuelve con valores útiles: Proporciona datos estructurados y útiles cuando resuelvas la Promise.
function obtenerEstadisticas() {
  return new Promise((resolve, reject) => {
    // Procesar datos...
    resolve({
      total: 150,
      activos: 120,
      inactivos: 30,
      ultimaActualizacion: new Date()
    });
  });
}

La creación de Promises te permite adaptar cualquier operación asíncrona al patrón de Promises, proporcionando una interfaz consistente y predecible para el manejo de operaciones asíncronas en tus aplicaciones Node.js.

Aprende Node online

Otras lecciones de Node

Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Node y certifícate

Ejercicios de programación de Node

Evalúa tus conocimientos de esta lección Promises con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.