The top-level await
(TLA) is a new Javascript feature that allows operator usage await
in the higher scope of Ecmascript modules (ESM). This higher scope is called top-level.
The behavior of await
in the module top-level is the same as when it is used in asynchronous functions. In short:
- In case the promise is resolved, the
await
returns the resolution value.
- In case the promise is rejected,
await
launches the rejection value.
What justifies the top-level await
?
In some cases, it may be necessary to load some resource asynchronously during a module startup. In order for this to be done on its own, the export
s of the module, in the ideal world, should not be effected until all asynchronous startup work has been completed.
That’s what, basically, the top-level await
resolve. When used in module top-level, prevent the execution of the rest of the module (including the export
s of the module) until the promise has been resolved.
This ensures that the module will only export its content when all the promises awaited on top-level have been resolved or any of them have been rejected. This "short-circuit" behavior is similar to Promise.all
.
Of course this feature should not be overused (we will see below some negative points of doing this resource initialization asynchronously). But when necessary, it can certainly be quite useful. The proposal repository (still in stage three) mentions some cases of use in which the top-level await
can make sense.
What are the precautions to be taken when using the top-level await
?
1. The top-level await
crashes a module and its descendants
This may be the worst problem facilitated for top-level await
. I must point out that the problem is not due to the TLA itself, but due to the fact of asynchronous resource loading on the top-level module.
There’s no way out of this - if a module To does asynchronous boot, it will lock until the completion of the awaited promises. And, because of this lock, all other modules that import To (using the syntax import ... from 'A'
) will also be stopped.
This locking by progeny can be extremely prolonged in long import chains. In some cases, use Dynamic Imports can avoid this problem if the module can be loaded so Lazy.
Supposing a.mjs
contains some kind of asynchronous boot, this module, which depends on A, will also be locked:
// Só será executado quando `A` terminar a inicialização...
import * as A from './a.mjs';
console.log(A);
But if the import is made using Dynamic import, which is asynchronous, this module will not be locked. Note that in this case the import must be done within an asynchronous function. If the await
be done in top-level, the behaviour would be the same as in the previous example.
// Note agora que o carregamento do módulo `A` é lazy.
// Desse modo, este módulo não será bloqueado em detrimento de `A`.
//
// O módulo `A` será carregado somente quando a função `doStuff` for invocada.
export async function doStuff() {
const A = await import('./a.mjs');
console.log(A);
}
I haven’t finished yet, but the time is half over. In a full time the answer. Nevertheless, what is already there is what I think is the most important.
– Luiz Felipe