Given `a.js` containing `console.log('a1'); await null; console.log('a2');`, `b.js` containing `console.log('b');`, and `main.js` containing `import './a.js'; import './b.js'; console.log('main');`, what does running `main.js` as an ES module print, and why?
answer
- dependencies run in source order
- suspension is local to one module
- sibling has nothing to wait for
- dependents wait, siblings do not
- importer body always runs last
basics
~10 sIt prints a1, b, a2, main. The await in a.js suspends only that module, so the independent sibling b.js evaluates immediately; main.js runs last because it waits for both dependencies to finish.
solid answer
~40 sThe output is `a1`, `b`, `a2`, `main`. Dependencies are evaluated in source order, so `a.js` starts first and prints `a1`, then hits the top-level `await` and suspends. Suspension is local to that module: nothing is blocked, so evaluation continues with the next dependency, and `b.js` runs to completion and prints `b`. Only `main.js` is held back, because a module's body cannot begin until all of its dependencies have finished evaluating, and `a.js` has not. When the awaited value settles, `a.js` resumes and prints `a2`, and with all dependencies now complete `main.js` runs and prints `main`. The lesson is that a top-level await delays ancestors, not unrelated siblings.
go deeper
Be able to trace which lines run before the await and which run after, and say that the importing module runs last. Knowing that dependencies evaluate before the file that imports them already gets you most of the way.
Explain the two rules that produce the order: suspension is local to the awaiting module, and a module body starts only once all its dependencies have finished. Being able to say why b lands between a1 and a2 is the whole question.
Turn it into diagnosis: when startup is slow, identify which modules are genuinely on the dependent path of an await and which merely look related. Show that you would not expect an unrelated sibling to be delayed and would look elsewhere if it were.
Own the structural implication for load performance: awaits placed on the entry module's dependency path serialize startup, while awaits in independent branches overlap. Shaping the import graph is therefore a real lever on time-to-first-work.
## The answer ``` a1 b a2 main ``` ## Walking the evaluation By the time evaluation begins, the graph has already been parsed and linked, so the engine knows that `main.js` depends on `a.js` and `b.js`, in that order. Module bodies are then evaluated depth-first, dependencies before dependents, in the order the imports appear. **`a.js` starts.** Its body runs normally and prints `a1`. Then it reaches `await null`. Awaiting a non-promise still suspends: the value is wrapped and resumption is scheduled for later, so the rest of `a.js` is deferred. `a.js` is now partway evaluated — `a1` has happened, `a2` has not — and control returns to the module system. **`b.js` runs.** This is the part people get wrong. Because the suspension in `a.js` returned control rather than blocking, the engine simply continues with the next thing it has to evaluate, which is `main.js`'s second dependency. `b.js` has no top-level await and no dependency on `a.js`, so it runs straight through and prints `b`. **`main.js` waits.** The rule that a module's body runs only after all its dependencies have finished evaluating is unchanged. `b.js` is finished but `a.js` is not, so `main.js` does not run yet. It is recorded as having a pending asynchronous dependency. **`a.js` resumes.** Once the awaited value settles, the remainder of `a.js` runs and prints `a2`. `a.js` is now fully evaluated, `main.js` has no pending dependencies left, so its body runs and prints `main`. ## The two rules that produce this Everything above follows from two statements worth memorizing: 1. A top-level await suspends **only the module it is written in**, and returns control so other evaluation can proceed. 2. A module's body begins only after **every one of its dependencies has finished evaluating**. Rule 1 explains why `b` is printed before `a2` — `b.js` is a sibling, not a dependent, so nothing about `a.js`'s pause concerns it. Rule 2 explains why `main` is last — `main.js` is a dependent, so it is exactly the thing that must wait. ## Why the sibling is not blocked This behaviour is deliberate. If a suspended module stopped the entire graph, adding one top-level await to a leaf module would serialize the load of everything else, including modules that have nothing to do with it. Letting independent parts of the graph proceed means the cost of an await is paid by the things that actually depend on its result. In a real application whose entry point imports several feature modules, one of them awaiting a configuration load does not prevent the others from evaluating; it only delays the entry module that needs them all. ## Variations that change the answer Swap the import order in `main.js` and the output becomes `b`, `a1`, `a2`, `main` — `b.js` now evaluates first because dependencies run in source order. Make `b.js` import `a.js` as well, and `b.js` becomes a dependent rather than a sibling: it can no longer run before `a.js` finishes, so its log moves after `a2`. Replace `await null` with an await of something that settles much later, and the shape of the output is identical — `b` still prints early, `main` still prints last — only the wall-clock gap grows. That is the useful mental model: top-level await does not reorder who waits, it only stretches how long the dependents wait. ## Why interviewers like it The puzzle separates two ideas that are easy to conflate: suspension and blocking. A candidate who thinks a top-level await blocks the graph predicts `a1`, `a2`, `b`, `main`. A candidate who thinks it merely schedules work and does not delay importers predicts `a1`, `b`, `main`, `a2`. Only someone holding both rules at once gets `a1`, `b`, `a2`, `main`, and the two wrong answers map cleanly onto the two common misconceptions.
- How does the output change if b.js also imports a.js?`b.js` stops being an independent sibling and becomes a dependent of `a.js`, so it cannot run until `a.js` has finished evaluating. The output becomes `a1`, `a2`, `b`, `main`. This is the same rule as before — a module's body waits for its dependencies — applied one level lower in the graph.
- Does awaiting a value that is not a promise, like `await null`, actually suspend the module?Yes. `await` on a non-promise still defers the remainder of the body rather than continuing straight through; the value is wrapped and resumption is scheduled. That is why `b` prints before `a2` in this example even though nothing genuinely asynchronous happened. The delay is minimal, but the ordering consequence is the same as for a real async operation.
- If a.js awaited a slow network call instead of null, would b.js still print early?Yes, and that is the point of the design. `b.js` is not a dependent of `a.js`, so it evaluates as soon as control returns from the suspension, regardless of how long the awaited operation takes. Only `main.js` — the module that actually depends on `a.js` — absorbs the full delay before its body runs.
saying these in an interview costs you the question
- Says the whole graph pauses at the top-level await
- Predicts main runs before a.js resumes
- Thinks `await null` continues without suspending
- Claims sibling modules are evaluated in parallel threads
- Assumes import order does not affect evaluation order