openlang

ECMA-262 never mentions an event loop

ECMA-262 never mentions an event loop. It defines jobs, and a host hook called HostEnqueuePromiseJob that the engine calls when a promise reaction is ready to run. When those jobs actually run, and what runs between them, is the host's business.

That's why setTimeout isn't in the language spec at all, and why arguing about whether a timer callback beats a .then callback can't be settled by reading ECMA-262 alone. The ordering everybody relies on, every promise job drained before the next timer task, comes from the HTML event loop and from Node, not from the core spec.

Still a useful split. Within one turn, promise reactions run in the order they were enqueued, and await on an already resolved value still waits for a job, so the code after it never runs synchronously. Dart has the same two-level shape, with work from scheduleMicrotask going ahead of queued events.

Hosts agree on more than they have to. I just wish fewer explanations started with a diagram the spec never drew.

The conversation continues 2

Follow the reasoning
  1. Permalink

    This maps almost one to one onto delta cycles. In VHDL a signal assignment never takes effect immediately. It schedules a new value, the simulator finishes every process that woke up, then updates the signals and wakes whatever is sensitive to them, all without advancing simulated time. Time moves only once no further delta cycles are pending.

    Read "signal update" as "promise job" and "time step" as "task" and the structure is the same. A .then chain that keeps enqueuing work starves timers the way a combinational loop pins a simulator to one time point. GHDL stops such a run once it hits its delta-cycle limit. A browser tab just hangs.

    One difference is worth keeping. Processes resumed in the same delta all read the same signal values, whatever order they run in. Promise jobs see each other's side effects at once.

  2. Permalink

    SystemVerilog makes the analogy closer still, because it splits each time slot into named regions. Active, Inactive, NBA, then Observed and Reactive for assertions and program blocks, plus a few more. A blocking assignment runs in Active, #0 moves a process to Inactive, and the update from a nonblocking <= waits for NBA.

    A testbench sprinkled with #0 to dodge a race is the same move as await null to let a pending .then go first. Works until somebody else adds one. Sample through a clocking block instead, so reads happen at a defined point relative to the clock.