Top-level await

No es necesario usar async en el contexto global


En principio, es posible que hayas leído que en Javascript sólo se puede utilizar un await en el interior de funciones declaradas como async. Por lo tanto, si tuvieramos que ejecutar una función asíncrona con await en nuestro fichero Javascript principal (contexto global), tendríamos que envolver el await en una función declarada con async.

Crear una función asíncrona sólo para utilizar un await se siente bastante artificial y complicado, y aunque la teoría es cierta, no siempre tiene que ser así.

¿Qué es top-level await?

En se añade una característica llamada Top-level await que permite el uso de await aunque no nos encontremos en funciones declaradas como async.

Para ello, debes cumplir dos condiciones:

  • Estar en el nivel superior, o sea, en el contexto global.
  • Debe tratarse de Javascript en un módulo ESM. Si estás cargando un .js en el navegador sin <script type="module"> o estás usando NodeJS con CommonJS no podrás usar esta característica.

Si no cumplimos las condiciones anteriores, es posible que nos aparezca un error similar a este en nuestro navegador:

Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

Esto ocurre porque, como bien dice el mensaje de error, estamos ejecutando await en un lugar que no es asíncrono, y debemos ejecutarlo en un contexto asíncrono.

Ejemplo sin top-level await

Veamos un ejemplo donde solicitamos el archivo /robots.txt, obtenemos su contenido y lo mostramos por consola:

<script>
  async function init() {
    const response = await fetch("/robots.txt");
    const data = await response.text();
    return data;
  }

  const data = await init();
  console.log(data);
</script>

Si pulsamos en DEMO, aunque veremos una pantalla blanca porque no renderiza nada, si vamos a la consola, veremos el error await is only valid in async functions and the top level bodies of modules.

Ejemplo con top-level await

Sin embargo, veamos ahora que ocurre si definimos el mismo código anterior en un módulo ESM, utilizando el <script type="module">. Si examinamos la consola del navegador, comprobaremos que si se muestra el contenido correctamente:

<script type="module">
  async function init() {
    const response = await fetch("/robots.txt");
    const data = await response.text();
    return data;
  }

  const data = await init();
  console.log(data);
</script>

Como hemos mencionado, el await si que se puede ejecutar en el contexto global sin necesidad de envolverlo en una función async si estamos en un módulo ESM.

Solución en otros entornos

Si estamos utilizando sistemas antiguos CommonJS, síncronos, o que simplemente no soportan el top-level await, se puede solucionar este problema envolviendo el código en una función asíncrona y auto-ejecutarla, como se muestra en el siguiente fragmento de código:

<script>
  async function init() {
    const response = await fetch("/robots.txt");
    const data = await response.text();
    return data;
  }

  (async function() {
    const data = await init();
    console.log(data);
  })();
</script>

Sin embargo, el código de la solución dista de ser una solución elegante. La mejor opción sería migrar a un sistema donde podamos utilizar top-level await:

  • Cargando Javascript con un <script type="module"> en lugar de un <script> (Navegador).
  • Añadiendo un "type": "module" en el package.json de nuestro proyecto (NodeJS).

Sin embargo, son cambios que requieren conocer de arquitectura o realizar profundos cambios en nuestro proyecto, por lo que sólo se podrían aplicar inmediatamente si nos encontramos empezando un nuevo proyecto.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev