Node
Tutorial Node: Async/Await
Aprende a usar async/await en JavaScript para simplificar código asíncrono, mejorar legibilidad y manejar errores de forma efectiva.
Aprende Node y certifícateSintaxis async/await
La sintaxis async/await representa una evolución natural en el manejo de operaciones asíncronas en JavaScript y Node.js. Esta característica permite escribir código asíncrono que se lee y estructura de manera similar al código síncrono, eliminando la complejidad visual de las cadenas de promesas.
Declaración de funciones asíncronas
Para utilizar await, primero debemos declarar una función como asíncrona mediante la palabra clave async
. Esta declaración transforma automáticamente la función para que devuelva una promesa, independientemente de lo que retorne explícitamente.
// Función asíncrona básica
async function obtenerDatos() {
return "Datos obtenidos";
}
// Equivale a:
function obtenerDatos() {
return Promise.resolve("Datos obtenidos");
}
Las funciones async pueden declararse de múltiples formas, manteniendo la flexibilidad de JavaScript:
// Declaración de función
async function procesarArchivo() {
// código asíncrono
}
// Expresión de función
const procesarArchivo = async function() {
// código asíncrono
};
// Función flecha
const procesarArchivo = async () => {
// código asíncrono
};
Uso de await para esperar promesas
La palabra clave await solo puede utilizarse dentro de funciones marcadas como async
. Su función es pausar la ejecución de la función hasta que la promesa se resuelva, devolviendo directamente el valor resuelto.
const fs = require('fs').promises;
async function leerArchivo() {
try {
// await pausa la ejecución hasta que la promesa se resuelve
const contenido = await fs.readFile('datos.txt', 'utf8');
console.log('Contenido del archivo:', contenido);
return contenido;
} catch (error) {
console.error('Error al leer archivo:', error.message);
}
}
Manejo de errores con try/catch
Una de las ventajas principales de async/await es el manejo natural de errores mediante bloques try/catch
, que resulta más intuitivo que el método .catch()
de las promesas.
const http = require('http');
async function realizarPeticion(url) {
try {
// Simulamos una petición HTTP
const respuesta = await new Promise((resolve, reject) => {
const req = http.get(url, (res) => {
let datos = '';
res.on('data', chunk => datos += chunk);
res.on('end', () => resolve(datos));
});
req.on('error', reject);
});
return JSON.parse(respuesta);
} catch (error) {
// Captura tanto errores de red como de parsing JSON
throw new Error(`Fallo en petición: ${error.message}`);
}
}
Operaciones secuenciales vs paralelas
Con async/await podemos controlar fácilmente si las operaciones asíncronas se ejecutan de forma secuencial o paralela, algo crucial para el rendimiento de nuestras aplicaciones.
Ejecución secuencial (una después de otra):
async function procesarArchivosSecuencial() {
const archivo1 = await fs.readFile('archivo1.txt', 'utf8');
const archivo2 = await fs.readFile('archivo2.txt', 'utf8');
const archivo3 = await fs.readFile('archivo3.txt', 'utf8');
return [archivo1, archivo2, archivo3];
}
Ejecución paralela (todas al mismo tiempo):
async function procesarArchivosParalelo() {
// Iniciamos todas las operaciones simultáneamente
const promesas = [
fs.readFile('archivo1.txt', 'utf8'),
fs.readFile('archivo2.txt', 'utf8'),
fs.readFile('archivo3.txt', 'utf8')
];
// Esperamos a que todas se completen
const resultados = await Promise.all(promesas);
return resultados;
}
Combinando async/await con módulos de Node.js
En el contexto de Node.js, async/await se integra perfectamente con los módulos nativos que devuelven promesas, como la versión promisificada del módulo fs
:
const fs = require('fs').promises;
const path = require('path');
async function organizarArchivos(directorio) {
try {
// Leer contenido del directorio
const archivos = await fs.readdir(directorio);
// Procesar cada archivo
for (const archivo of archivos) {
const rutaCompleta = path.join(directorio, archivo);
const stats = await fs.stat(rutaCompleta);
if (stats.isFile()) {
console.log(`Archivo: ${archivo} - Tamaño: ${stats.size} bytes`);
} else if (stats.isDirectory()) {
console.log(`Directorio: ${archivo}`);
// Recursión con async/await
await organizarArchivos(rutaCompleta);
}
}
} catch (error) {
console.error(`Error procesando directorio ${directorio}:`, error.message);
}
}
Retorno de valores en funciones async
Las funciones async siempre devuelven una promesa. Si la función retorna un valor directamente, este se envuelve automáticamente en una promesa resuelta. Si se lanza una excepción, la promesa se rechaza.
async function calcularResultado(numero) {
if (numero < 0) {
throw new Error('Número debe ser positivo');
}
// Simulamos una operación asíncrona
await new Promise(resolve => setTimeout(resolve, 100));
return numero * 2; // Se convierte automáticamente en Promise.resolve(numero * 2)
}
// Uso de la función
async function main() {
try {
const resultado = await calcularResultado(5);
console.log('Resultado:', resultado); // 10
} catch (error) {
console.error('Error:', error.message);
}
}
Esta sintaxis moderna y limpia hace que el código asíncrono sea más legible y mantenible, especialmente en aplicaciones Node.js que manejan múltiples operaciones de entrada/salida como lectura de archivos, consultas a bases de datos o peticiones HTTP.
Ventajas sobre Promises
La adopción de async/await sobre el uso directo de promesas representa un salto cualitativo en la experiencia de desarrollo. Aunque ambos enfoques manejan la asincronía de manera efectiva, async/await ofrece beneficios tangibles que mejoran tanto la productividad del desarrollador como la calidad del código.
Legibilidad y mantenibilidad del código
La legibilidad es quizás la ventaja más evidente de async/await. Mientras que las promesas requieren encadenar métodos .then()
y .catch()
, async/await permite escribir código que fluye de manera natural, similar al código síncrono.
Con promesas tradicionales:
const fs = require('fs').promises;
function procesarDatos() {
return fs.readFile('config.json', 'utf8')
.then(contenido => JSON.parse(contenido))
.then(config => {
return fs.readFile(config.archivoUsuarios, 'utf8');
})
.then(usuarios => JSON.parse(usuarios))
.then(datosUsuarios => {
return datosUsuarios.filter(usuario => usuario.activo);
})
.catch(error => {
console.error('Error en procesamiento:', error);
throw error;
});
}
Con async/await:
const fs = require('fs').promises;
async function procesarDatos() {
try {
const contenido = await fs.readFile('config.json', 'utf8');
const config = JSON.parse(contenido);
const usuarios = await fs.readFile(config.archivoUsuarios, 'utf8');
const datosUsuarios = JSON.parse(usuarios);
return datosUsuarios.filter(usuario => usuario.activo);
} catch (error) {
console.error('Error en procesamiento:', error);
throw error;
}
}
Manejo simplificado de errores
El manejo de errores con async/await utiliza la estructura familiar de try/catch
, eliminando la necesidad de múltiples bloques .catch()
o la propagación manual de errores a través de la cadena de promesas.
Gestión de errores específicos con promesas:
function conectarBaseDatos() {
return conectar()
.then(conexion => {
return autenticar(conexion);
})
.then(sesion => {
return obtenerDatos(sesion);
})
.catch(error => {
if (error.code === 'AUTH_FAILED') {
console.error('Fallo de autenticación');
} else if (error.code === 'CONNECTION_TIMEOUT') {
console.error('Timeout de conexión');
}
throw error;
});
}
Gestión equivalente con async/await:
async function conectarBaseDatos() {
try {
const conexion = await conectar();
const sesion = await autenticar(conexion);
const datos = await obtenerDatos(sesion);
return datos;
} catch (error) {
if (error.code === 'AUTH_FAILED') {
console.error('Fallo de autenticación');
} else if (error.code === 'CONNECTION_TIMEOUT') {
console.error('Timeout de conexión');
}
throw error;
}
}
Depuración más efectiva
La depuración con async/await resulta considerablemente más sencilla. Los stack traces son más claros y las herramientas de desarrollo pueden establecer breakpoints de manera más intuitiva en cada línea de código asíncrono.
const http = require('http');
async function obtenerDatosUsuario(id) {
// Breakpoint aquí funciona como esperamos
const perfil = await obtenerPerfil(id);
// Breakpoint aquí también es directo
const configuracion = await obtenerConfiguracion(perfil.tipo);
// El stack trace muestra claramente la secuencia
const preferencias = await obtenerPreferencias(id);
return {
perfil,
configuracion,
preferencias
};
}
Mejor control de flujo condicional
Las estructuras de control como bucles y condicionales se integran naturalmente con async/await, algo que resulta complejo y poco elegante con promesas encadenadas.
Bucle con condiciones usando promesas:
function procesarArchivosConPromesas(archivos) {
let promesa = Promise.resolve();
archivos.forEach(archivo => {
promesa = promesa.then(() => {
return fs.stat(archivo);
}).then(stats => {
if (stats.size > 1000000) {
return comprimirArchivo(archivo);
}
return Promise.resolve();
});
});
return promesa;
}
Bucle equivalente con async/await:
async function procesarArchivos(archivos) {
for (const archivo of archivos) {
const stats = await fs.stat(archivo);
if (stats.size > 1000000) {
await comprimirArchivo(archivo);
}
}
}
Composición y reutilización de funciones
La composición de funciones asíncronas se vuelve más natural con async/await, permitiendo crear abstracciones más limpias y reutilizables.
// Funciones auxiliares reutilizables
async function validarArchivo(ruta) {
const stats = await fs.stat(ruta);
return stats.isFile() && stats.size > 0;
}
async function procesarContenido(contenido) {
// Simulamos procesamiento asíncrono
await new Promise(resolve => setTimeout(resolve, 100));
return contenido.toUpperCase();
}
// Función principal que compone las anteriores
async function procesarArchivoCompleto(ruta) {
const esValido = await validarArchivo(ruta);
if (!esValido) {
throw new Error('Archivo no válido');
}
const contenido = await fs.readFile(ruta, 'utf8');
const procesado = await procesarContenido(contenido);
await fs.writeFile(`${ruta}.procesado`, procesado);
return `Archivo procesado: ${ruta}`;
}
Integración con APIs modernas de Node.js
Las APIs modernas de Node.js están diseñadas pensando en async/await, proporcionando una experiencia de desarrollo más cohesiva y predecible.
const { pipeline } = require('stream').promises;
const fs = require('fs');
async function procesarStreamArchivos() {
try {
// Pipeline de streams con async/await
await pipeline(
fs.createReadStream('entrada.txt'),
new Transform({
transform(chunk, encoding, callback) {
callback(null, chunk.toString().toUpperCase());
}
}),
fs.createWriteStream('salida.txt')
);
console.log('Pipeline completado exitosamente');
} catch (error) {
console.error('Error en pipeline:', error);
}
}
Estas ventajas hacen que async/await sea la opción preferida para el desarrollo moderno en Node.js, especialmente en aplicaciones que requieren múltiples operaciones asíncronas coordinadas y un mantenimiento a largo plazo del código.
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.
Instalación De Node.js
Introducción Y Entorno
Fundamentos Del Entorno Node.js
Introducción Y Entorno
Estructura De Proyecto Y Package.json
Introducción Y Entorno
Introducción A Node
Introducción Y Entorno
Gestor De Versiones Nvm
Introducción Y Entorno
Repl De Nodejs
Introducción Y Entorno
Ejercicios de programación de Node
Evalúa tus conocimientos de esta lección Async/Await con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.