⏱️ 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
  1. Requisitos para usar top-level await
  2. Cómo funciona internamente
  3. Casos de uso prácticos
    1. 1. Carga de configuración
    2. 2. Conexión a base de datos
    3. 3. Inicialización condicional de dependencias
    4. 4. Feature flags desde un servicio remoto
  4. Cuándo NO usar top-level await
    1. 1. En librerías/paquetes npm
    2. 2. Operaciones que pueden fallar silenciosamente
    3. 3. Múltiples awaits secuenciales independientes
  5. Top-level await vs CommonJS
  6. Compatibilidad
  7. Errores comunes y cómo solucionarlos
    1. SyntaxError: await is only valid in async functions
    2. ERR_REQUIRE_ESM
    3. El módulo tarda mucho en cargar
  8. Preguntas frecuentes
    1. ¿Top-level await bloquea el event loop?
    2. ¿Puedo usar top-level await en TypeScript?
    3. ¿Es seguro usar top-level await en producción?
  9. Referencias

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:

  1. Node.js 16+ (o 14.8+ con --harmony-top-level-await)
  2. 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 tu package.json no tiene "type": "module" y tu archivo es .js, Node.js lo trata como CommonJS y await en 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:

  1. Parsea el módulo y detecta los await de nivel superior
  2. Ejecuta el código sincrónicamente hasta encontrar el primer await
  3. Pausa la ejecución del módulo (no del proceso entero)
  4. Resuelve la Promise
  5. 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:

  1. Migrar a ESM — agregar "type": "module" al package.json y cambiar require por import
  2. Usar import() dinámico — CommonJS sí soporta import() async
// CommonJS con import() dinámico
async function main() {
  const { config } = await import('./config.mjs');
  console.log(config);
}
main();

Compatibilidad

RuntimeSoporteDesde
Node.js✅ Establev16.0.0 (abril 2021)
Deno✅ Nativov1.0 (mayo 2020)
Bun✅ Nativov0.1 (julio 2022)
Chrome/Edgev89 (marzo 2021)
Firefoxv89 (junio 2021)
Safariv15 (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

Síguenos en t.me/programacion para más contenido tech diario.


0 Comentarios

Deja un comentario

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.