Update – in the comments below, @bmeck clarifies that statically imported modules do evaluate in a deterministic sequential order, contra the Hacker News comments that prompted this gist.
Another follow-up to Top-level await is a footgun. On the Hacker News comments, Domenic says this:
(The argument also seems to be predicated on some fundamental misunderstandings, in that it thinks everything would have to be sequentialized. See my other replies throughout this thread.)
In other words, if you have an app like this...
// main.js
import './foo.js';
import './bar.js';
// foo.js
await delay( Math.random() * 1000 );
polyfillSomeFeature();
// bar.js
await delay( Math.random() * 1000 );
useThePolyfilledFeature();
...then the order is random, breaking any guarantees you might have thought you had about the order of execution.
Let's talk about another way that could break your app.
It's not uncommon for cyclical dependencies to exist between modules:
// A.js
import B from './B.js';
export default class A {
b () {
return new B();
}
}
// B.js
import A from './A.js';
export default class B extends A {
a () {
return new A();
}
}
Note that the cyclical dependency is assymetrical – while A.js
depends on B.js
, B.js
can't even execute until A.js
has executed. This means you have to be careful about which module gets imported first:
// main.js
import A from './A.js';
import B from './B.js';
Modules are loaded and evaluated in a specific order. In this case, main.js
first imports A.js
. Before A.js
can evaluate, its dependency B.js
must be loaded and evaluated. The dependency on A.js
is ignored because that module is already in the loading phase, so its exports are undefined – which means that B.js
, which can't execute without A.js
, fails altogether, and your app blows up.
But because that happens deterministically, it's possible to track down the error and fix it:
// main.js
import B from './B.js';
import A from './A.js';
Now let's throw some asynchronous fairy dust on this whole thing:
// main.js
import A from './foo.js';
import B from './bar.js';
// foo.js
await delay( Math.random() * 1000 );
const A = await import( './A.js' );
export default A;
// bar.js
await delay( Math.random() * 1000 );
const B = await import( './B.js' );
export default B;
Just like that, your entire app has become vulnerable to a race condition (which in a less contrived situation would be a lot more sneaky).
While it's true that you could blame both situations on the developer (bar.js
in the first example should have made its dependency on the polyfill explicit, even though that's not generally how it works, and you could engineer the same race condition with the cyclical dependencies without await
), the point here is that we've just made the behaviour of our programs much harder to reason about. These are just two simple examples of the confusion that could arise – now extrapolate that to a large application with many dependencies.
...which is that top-level await by its very nature slows down your app startup. It just mitigates it. So you have three choices:
- Top-level
await
that slows your app down and has crazy non-deterministic execution - Top-level
await
that slows your app down even more, but at least runs in a predictable way - Don't allow top-level
await
into the language
phew! That's good to hear. In which case, my original concerns stand, and the objections that prompted this gist were off the mark, because they weren't responding to the actual argument. Hey ho.
Granted – my goal here is to argue that dynamic loading could be done with configurable resolution. It doesn't sound like there are any insurmountable technical obstacles to that.
This is the obvious objection, yes. I think people typically overstate the configuration burden (perhaps a case of 'once bitten twice shy' after dealing with RequireJS configs – though it's a very different world now). I also think that developers have perhaps become a little too accustomed to tools like Browserify and Webpack sparing them from having to think about this stuff. It really doesn't seem like too much to expect that when you include a dependency in your application, you might have to actually know what it contains and possibly engage in a little light (but standardised and documented) configuration. (In fact devs already do this without complaint in some cases, e.g. having to set up
webpack.DefinePlugin
for production builds of React, or whatever. It's the same principle.)We've managed to build these apps without top-level
await
so far.await
gives us nicer syntax, I totally agree. I think the main place where we differ is on the cost – I think it's too high, and you don't – and on the feasibility of the alternative (which, to be clear, has been proven to work in the form of RequireJS configs, which were a much grubbier version of what I'm talking about).I definitely want to be able to use imperative imports in my applications. I just don't want my app to be blocked by them.