# 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! 🚀
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