⏱️ Lectura: 8 min
Cuando JavaScript introdujo async/await en ES2017, cambió la forma en que escribimos código asíncrono. Pero tenía una limitación frustrante: await solo funcionaba dentro de una función async. Si querías usarlo en el nivel superior de un archivo, tenías que envolver todo en una IIFE.
📑 En este artículo
Top-level await elimina esa restricción. Desde Node.js 14.8+ (con flag) y Node.js 16+ (estable), puedes usar await directamente en el nivel superior de un módulo ES — sin wrappers, sin hacks.
// Antes ❌ — IIFE obligatoria
(async () => {
const data = await fetch('https://api.example.com/config');
const config = await data.json();
console.log(config);
})();
// Ahora ✅ — top-level await
const data = await fetch('https://api.example.com/config');
const config = await data.json();
console.log(config);
Parece un cambio menor, pero tiene implicaciones profundas en cómo estructuramos aplicaciones Node.js. En este artículo vas a entender exactamente qué es, cómo funciona internamente, cuándo usarlo y — igual de importante — cuándo no usarlo.
Requisitos para usar top-level await
No funciona en cualquier archivo JavaScript. Necesitas dos cosas:
- Node.js 16+ (o 14.8+ con
--harmony-top-level-await) - Módulo ES — el archivo debe ser un módulo, no un script CommonJS
Para que Node.js trate tu archivo como módulo ES, tienes tres opciones:
// Opción 1: package.json con "type": "module"
{
"name": "mi-proyecto",
"type": "module"
}
# Opción 2: extensión .mjs
node app.mjs
# Opción 3: flag --input-type (para stdin o eval)
node --input-type=module -e "const x = await Promise.resolve(42); console.log(x);"
⚠️ Importante: Si tupackage.jsonno tiene"type": "module"y tu archivo es.js, Node.js lo trata como CommonJS yawaiten el nivel superior daráSyntaxError: await is only valid in async functions.
Cómo funciona internamente
Cuando usas top-level await, el módulo se comporta como una función async gigante. El motor de JavaScript:
- Parsea el módulo y detecta los
awaitde nivel superior - Ejecuta el código sincrónicamente hasta encontrar el primer
await - Pausa la ejecución del módulo (no del proceso entero)
- Resuelve la Promise
- Continúa ejecutando el resto del módulo
Lo crucial: cualquier módulo que importe a un módulo con top-level await también espera. Es decir, el await se propaga por el grafo de dependencias.
// config.mjs — tiene top-level await
export const db = await connectToDatabase();
export const cache = await initRedis();
console.log('Config lista');
// app.mjs — importa config
import { db, cache } from './config.mjs';
// Esta línea NO se ejecuta hasta que config.mjs termine
console.log('App iniciada con DB y cache listos');
💡 Dato clave: Los módulos con top-level await no bloquean el event loop. Solo bloquean la evaluación de los módulos que dependen de ellos. Otros módulos independientes en el grafo se ejecutan normalmente en paralelo.
Casos de uso prácticos
1. Carga de configuración
El caso más común: leer configuración antes de que el resto de la app la necesite.
// config.mjs
import { readFile } from 'node:fs/promises';
const raw = await readFile('./config.json', 'utf-8');
export const config = JSON.parse(raw);
// Validación inmediata
if (!config.databaseUrl) {
throw new Error('DATABASE_URL requerida en config.json');
}
Sin top-level await, tendrías que exportar una función async o una Promise, y cada consumidor tendría que hacer await explícito.
2. Conexión a base de datos
// db.mjs
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
});
// Verificar conexión al importar
await pool.query('SELECT 1');
console.log('PostgreSQL conectado');
export default pool;
3. Inicialización condicional de dependencias
// logger.mjs
let logger;
if (process.env.NODE_ENV === 'production') {
const { default: pino } = await import('pino');
logger = pino({ level: 'info' });
} else {
logger = console;
}
export default logger;
Esto permite importar dependencias pesadas solo cuando se necesitan, reduciendo el tiempo de carga en desarrollo.
4. Feature flags desde un servicio remoto
// features.mjs
const res = await fetch('https://api.features.example.com/flags', {
headers: { Authorization: `Bearer ${process.env.FEATURE_API_KEY}` },
});
export const flags = await res.json();
// Uso en otros módulos
import { flags } from './features.mjs';
if (flags.newCheckoutFlow) {
// Nueva experiencia de checkout
}
Cuándo NO usar top-level await
Top-level await es poderoso pero tiene trampas. Estos son los casos donde es mejor evitarlo:
1. En librerías/paquetes npm
// ❌ NO hagas esto en una librería
// mi-libreria/index.mjs
const response = await fetch('https://api.example.com/data');
export const data = await response.json();
Si tu librería hace un top-level await, todos los consumidores se bloquean esperando que tu fetch termine. No puedes controlar la velocidad de esa red desde la librería del usuario. Exporta una función async en su lugar:
// ✅ Mejor: exportar función async
export async function getData() {
const response = await fetch('https://api.example.com/data');
return response.json();
}
2. Operaciones que pueden fallar silenciosamente
// ❌ Si el fetch falla, el módulo entero falla
// y cualquier módulo que lo importe también
const res = await fetch('https://api.example.com/optional-data');
export const optionalData = await res.json();
Un error en top-level await mata la carga del módulo. Si el dato es opcional, usa try/catch:
// ✅ Con manejo de errores
let optionalData = null;
try {
const res = await fetch('https://api.example.com/optional-data');
optionalData = await res.json();
} catch (err) {
console.warn('Datos opcionales no disponibles:', err.message);
}
export { optionalData };
3. Múltiples awaits secuenciales independientes
// ❌ Secuencial — cada await espera al anterior
const users = await fetchUsers(); // 200ms
const products = await fetchProducts(); // 300ms
const orders = await fetchOrders(); // 250ms
// Total: ~750ms
// ✅ Paralelo con Promise.all — mucho más rápido
const [users, products, orders] = await Promise.all([
fetchUsers(), // 200ms
fetchProducts(), // 300ms ─┐
fetchOrders(), // 250ms ─┤ en paralelo
]);
// Total: ~300ms (el más lento)
💡 Tip: Siempre pregúntate: ¿estos awaits dependen uno del otro? Si no, usa Promise.all(). La diferencia puede ser enorme — en el ejemplo de arriba, 750ms vs 300ms.
Top-level await vs CommonJS
CommonJS (require()) es síncrono por diseño. No soporta top-level await y nunca lo hará:
// CommonJS — esto NO funciona
const data = await fetch('...'); // SyntaxError
module.exports = data;
Si tu proyecto usa CommonJS y quieres top-level await, tienes dos opciones:
- Migrar a ESM — agregar
"type": "module"alpackage.jsony cambiarrequireporimport - Usar
import()dinámico — CommonJS sí soportaimport()async
// CommonJS con import() dinámico
async function main() {
const { config } = await import('./config.mjs');
console.log(config);
}
main();
Compatibilidad
| Runtime | Soporte | Desde |
|---|---|---|
| Node.js | ✅ Estable | v16.0.0 (abril 2021) |
| Deno | ✅ Nativo | v1.0 (mayo 2020) |
| Bun | ✅ Nativo | v0.1 (julio 2022) |
| Chrome/Edge | ✅ | v89 (marzo 2021) |
| Firefox | ✅ | v89 (junio 2021) |
| Safari | ✅ | v15 (septiembre 2021) |
En 2026, top-level await tiene soporte universal en todos los runtimes y navegadores modernos. No necesitas polyfills ni transpilación.
Errores comunes y cómo solucionarlos
SyntaxError: await is only valid in async functions
Causa: El archivo no es un módulo ES. Solución: agregar "type": "module" al package.json o renombrar a .mjs.
ERR_REQUIRE_ESM
Causa: Intentas usar require() para importar un módulo ESM que tiene top-level await. Solución: usar import estático o import() dinámico.
El módulo tarda mucho en cargar
Causa: Un top-level await hace un fetch a un servidor lento y bloquea toda la cadena de imports. Solución: agregar timeout o mover la operación lenta a una función async explícita.
// Agregar timeout a un top-level await
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch('https://api.slow.com/data', {
signal: controller.signal,
});
export var data = await res.json();
} catch (err) {
console.error('Timeout o error cargando datos:', err.message);
export var data = null;
} finally {
clearTimeout(timeout);
}
Preguntas frecuentes
¿Top-level await bloquea el event loop?
No. Solo bloquea la evaluación de los módulos que dependen del módulo con el await. El event loop sigue corriendo normalmente — otros timers, conexiones y callbacks se procesan sin interrupción.
¿Puedo usar top-level await en TypeScript?
Sí. Configura "module": "es2022" o superior en tu tsconfig.json, y "target": "es2017" o superior. TypeScript compila top-level await sin problemas desde la versión 3.8.
¿Es seguro usar top-level await en producción?
Sí. Tiene soporte estable en Node.js desde 2021 (v16). En 2026 es una feature madura con 5 años de uso en producción. La clave es usarlo con manejo de errores adecuado y evitarlo en librerías públicas.
Referencias
- Node.js Docs — Top-Level Await
- MDN — await (Top-Level)
- TC39 Proposal — Top-Level Await
- V8 Blog — Top-Level Await
Síguenos en t.me/programacion para más contenido tech diario.
0 Comentarios