TechLead
Lección 6 de 6
8 min de lectura
Node.js

Patrones Asíncronos de Node.js

Domina callbacks, promises, async/await y manejo de errores

# Patrones Asíncronos de Node.js Node.js es asíncrono por naturaleza. Entender los patrones asíncronos es esencial para escribir código eficiente y no bloqueante. Aprende callbacks, promises, async/await y mejores prácticas. ## ¿Por Qué Asíncrono? Node.js usa un modelo de E/S no bloqueante que permite alta concurrencia: ```javascript // Código Síncrono (Bloqueante) const data1 = readFileSync('file1.txt'); // Espera const data2 = readFileSync('file2.txt'); // Espera const data3 = readFileSync('file3.txt'); // Espera // Tiempo total: ~300ms // Código Asíncrono (No Bloqueante) readFile('file1.txt', callback1); // No espera readFile('file2.txt', callback2); // No espera readFile('file3.txt', callback3); // No espera // Tiempo total: ~100ms (en paralelo) ``` ## Callbacks El patrón asíncrono original de Node.js: ### Callback Básico ```javascript const fs = require('fs'); // Leer archivo con callback fs.readFile('file.txt', 'utf8', (err, data) => { if (err) { console.error('Error:', err); return; } console.log('Datos:', data); }); console.log('Este mensaje aparece primero'); ``` ### Infierno de Callbacks Múltiples operaciones asíncronas anidadas: ```javascript // ❌ Código Difícil de Leer (Pirámide de la Perdición) fs.readFile('file1.txt', 'utf8', (err, data1) => { if (err) return console.error(err); fs.readFile('file2.txt', 'utf8', (err, data2) => { if (err) return console.error(err); fs.readFile('file3.txt', 'utf8', (err, data3) => { if (err) return console.error(err); console.log(data1, data2, data3); }); }); }); ``` ## Promises Las Promises proporcionan una mejor forma de manejar código asíncrono: ### Crear una Promise ```javascript function readFilePromise(filename) { return new Promise((resolve, reject) => { fs.readFile(filename, 'utf8', (err, data) => { if (err) { reject(err); } else { resolve(data); } }); }); } // Usar readFilePromise('file.txt') .then(data => { console.log(data); }) .catch(err => { console.error(err); }); ``` ### Encadenar Promises ```javascript // ✅ Mucho Mejor que Callbacks Anidados readFilePromise('file1.txt') .then(data1 => { console.log('Archivo 1:', data1); return readFilePromise('file2.txt'); }) .then(data2 => { console.log('Archivo 2:', data2); return readFilePromise('file3.txt'); }) .then(data3 => { console.log('Archivo 3:', data3); }) .catch(err => { console.error('Error:', err); }); ``` ### Promise.all() Ejecutar múltiples promises en paralelo: ```javascript const promise1 = readFilePromise('file1.txt'); const promise2 = readFilePromise('file2.txt'); const promise3 = readFilePromise('file3.txt'); Promise.all([promise1, promise2, promise3]) .then(([data1, data2, data3]) => { console.log('Todos los archivos:', data1, data2, data3); }) .catch(err => { console.error('Error en uno de los archivos:', err); }); ``` ### Promise.race() La primera promise en completarse gana: ```javascript const promise1 = new Promise(resolve => setTimeout(() => resolve('uno'), 100)); const promise2 = new Promise(resolve => setTimeout(() => resolve('dos'), 50)); Promise.race([promise1, promise2]) .then(result => { console.log(result); // 'dos' (completa primero) }); ``` ### Promise.allSettled() Espera a que todas las promises se completen (sin importar éxito/fallo): ```javascript const promises = [ readFilePromise('exists.txt'), readFilePromise('not-exists.txt'), readFilePromise('another.txt') ]; Promise.allSettled(promises) .then(results => { results.forEach(result => { if (result.status === 'fulfilled') { console.log('Éxito:', result.value); } else { console.log('Error:', result.reason); } }); }); ``` ### Promise.any() Primera promise exitosa: ```javascript const promises = [ fetch('https://api1.com/data'), fetch('https://api2.com/data'), fetch('https://api3.com/data') ]; Promise.any(promises) .then(response => { console.log('Primera respuesta exitosa:', response); }) .catch(err => { console.log('Todas las promises fallaron'); }); ``` ## Async/Await Sintaxis moderna que hace que el código asíncrono se vea síncrono: ### Sintaxis Básica ```javascript const fs = require('fs/promises'); // ✅ Código Limpio y Legible async function readFiles() { try { const data1 = await fs.readFile('file1.txt', 'utf8'); console.log('Archivo 1:', data1); const data2 = await fs.readFile('file2.txt', 'utf8'); console.log('Archivo 2:', data2); const data3 = await fs.readFile('file3.txt', 'utf8'); console.log('Archivo 3:', data3); } catch (err) { console.error('Error:', err); } } readFiles(); ``` ### Async/Await en Paralelo ```javascript // ❌ Secuencial (más lento) async function sequential() { const data1 = await fs.readFile('file1.txt', 'utf8'); // Espera 100ms const data2 = await fs.readFile('file2.txt', 'utf8'); // Espera 100ms const data3 = await fs.readFile('file3.txt', 'utf8'); // Espera 100ms // Tiempo total: ~300ms } // ✅ Paralelo (más rápido) async function parallel() { const [data1, data2, data3] = await Promise.all([ fs.readFile('file1.txt', 'utf8'), fs.readFile('file2.txt', 'utf8'), fs.readFile('file3.txt', 'utf8') ]); // Tiempo total: ~100ms } ``` ### Async en Arrow Functions ```javascript // Arrow function asíncrona const readFile = async (filename) => { const data = await fs.readFile(filename, 'utf8'); return data; }; // Usar readFile('file.txt') .then(data => console.log(data)) .catch(err => console.error(err)); ``` ### Async en Métodos de Clase ```javascript class FileManager { async readFile(filename) { try { const data = await fs.readFile(filename, 'utf8'); return data; } catch (err) { console.error('Error:', err); throw err; } } async writeFile(filename, content) { await fs.writeFile(filename, content, 'utf8'); } } // Usar const fm = new FileManager(); const data = await fm.readFile('file.txt'); ``` ## Manejo de Errores ### Try/Catch con Async/Await ```javascript async function handleErrors() { try { const data = await fs.readFile('file.txt', 'utf8'); const result = JSON.parse(data); return result; } catch (err) { // Manejar error específico if (err.code === 'ENOENT') { console.error('Archivo no encontrado'); } else if (err instanceof SyntaxError) { console.error('JSON inválido'); } else { console.error('Error desconocido:', err); } throw err; // Re-lanzar si es necesario } } ``` ### Promise Catch ```javascript readFilePromise('file.txt') .then(data => JSON.parse(data)) .then(result => console.log(result)) .catch(err => { console.error('Error:', err); }) .finally(() => { console.log('Limpieza o operaciones finales'); }); ``` ### Múltiples Operaciones Try/Catch ```javascript async function complexOperation() { let connection; try { // Conectar a base de datos connection = await database.connect(); // Operaciones de base de datos const user = await connection.findUser('123'); const posts = await connection.findPosts(user.id); return { user, posts }; } catch (err) { console.error('Error en operación:', err); throw err; } finally { // Siempre limpiar if (connection) { await connection.close(); } } } ``` ## Utilidades Asíncronas ### Promisify Convertir funciones callback a promises: ```javascript const { promisify } = require('util'); const fs = require('fs'); // Convertir fs.readFile a promise const readFilePromise = promisify(fs.readFile); // Usar con async/await async function readFile() { const data = await readFilePromise('file.txt', 'utf8'); console.log(data); } ``` ### Retraso (Delay) ```javascript // Función de retraso const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // Usar async function withDelay() { console.log('Inicio'); await delay(2000); // Espera 2 segundos console.log('Después de 2 segundos'); } ``` ### Timeout ```javascript // Promise con timeout function withTimeout(promise, ms) { const timeout = new Promise((_, reject) => { setTimeout(() => reject(new Error('Timeout')), ms); }); return Promise.race([promise, timeout]); } // Usar try { const data = await withTimeout( fetch('https://api.com/slow-endpoint'), 5000 // Timeout en 5 segundos ); } catch (err) { console.error('Solicitud agotó el tiempo:', err); } ``` ### Reintentos (Retry) ```javascript async function retry(fn, maxAttempts = 3, delay = 1000) { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { return await fn(); } catch (err) { if (attempt === maxAttempts) { throw err; } console.log(`Intento ${attempt} falló, reintentando...`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // Usar const data = await retry( () => fetch('https://api.com/unreliable'), 3, 2000 ); ``` ## Ejemplo del Mundo Real API completa con operaciones asíncronas: ```javascript const express = require('express'); const fs = require('fs/promises'); const path = require('path'); const app = express(); app.use(express.json()); const DATA_FILE = path.join(__dirname, 'data.json'); // Leer datos async function readData() { try { const data = await fs.readFile(DATA_FILE, 'utf8'); return JSON.parse(data); } catch (err) { if (err.code === 'ENOENT') { return []; // Archivo no existe, retornar array vacío } throw err; } } // Escribir datos async function writeData(data) { await fs.writeFile(DATA_FILE, JSON.stringify(data, null, 2), 'utf8'); } // Rutas app.get('/api/users', async (req, res) => { try { const users = await readData(); res.json(users); } catch (err) { res.status(500).json({ error: err.message }); } }); app.get('/api/users/:id', async (req, res) => { try { const users = await readData(); const user = users.find(u => u.id === parseInt(req.params.id)); if (!user) { return res.status(404).json({ error: 'Usuario no encontrado' }); } res.json(user); } catch (err) { res.status(500).json({ error: err.message }); } }); app.post('/api/users', async (req, res) => { try { const users = await readData(); const newUser = { id: users.length + 1, ...req.body }; users.push(newUser); await writeData(users); res.status(201).json(newUser); } catch (err) { res.status(500).json({ error: err.message }); } }); app.delete('/api/users/:id', async (req, res) => { try { let users = await readData(); const index = users.findIndex(u => u.id === parseInt(req.params.id)); if (index === -1) { return res.status(404).json({ error: 'Usuario no encontrado' }); } users.splice(index, 1); await writeData(users); res.status(204).send(); } catch (err) { res.status(500).json({ error: err.message }); } }); // Manejo de errores global app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Algo salió mal!' }); }); const PORT = 3000; app.listen(PORT, () => { console.log(`Servidor ejecutándose en puerto ${PORT}`); }); ``` ## Mejores Prácticas 1. **Preferir Async/Await**: Más legible que promises encadenadas 2. **Siempre Manejar Errores**: Usar try/catch con async/await 3. **Paralelo Cuando Sea Posible**: Usar Promise.all() para operaciones independientes 4. **Evitar Callbacks Anidados**: Refactorizar a promises o async/await 5. **Finally para Limpieza**: Usar finally para limpieza de recursos 6. **Timeouts**: Implementar timeouts para operaciones de red 7. **Reintentos**: Agregar lógica de reintentos para operaciones no confiables ## Recursos - [Documentación de MDN Async/Await](https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Statements/async_function) - [Node.js Promises](https://nodejs.org/docs/latest/api/promises.html) - [Guía de Callbacks vs Promises vs Async/Await](https://nodejs.dev/learn/understanding-javascript-promises) ¡Domina los patrones asíncronos para escribir código Node.js eficiente! 🚀

Continúa Aprendiendo