Event async / await

Many of JavaScript's interfaces are event-based and not immediately compatible with async / await.
The EventTarget class could be extended to return a Promise or an async iterator.
Their syntax could resemble the syntax of addEventListener.

Usage

const myButton = document.querySelector( "button" );

//await a single event:
let nextClick = await myButton.listenForEvent( "click" );
console.log( "Got a click: ", nextClick );

//await a stream of events:
for await ( const click of myButton.streamEvents( "click" ) ) {
    console.log( "Got another click: ", click );
}

Polyfill

listenForEvent( eventName ) would wrap a Promise around a single-use event listener.

EventTarget.prototype.listenForEvent =
    function( eventName ) {
        return new Promise( resolve => {
            const asyncEventListener = event => {
                this.removeEventListener( eventName, asyncEventListener );
                resolve( event );
            };
            this.addEventListener( eventName, asyncEventListener );
        } );
    }

And the async iterator generator streamEvents would yield a stream of those Promises:

EventTarget.prototype.streamEvents =
    async function*( eventName ) {
        while( true ) {
            yield await this.listenForEvent( eventName );
        }
    }

With these two functions, all interfaces based on EventTarget would become compatible with async / await syntax.

1 Like

EventTarget isn't part of the language, it's part of HTML (and presumably WinterCG as well), so you'd need to ask in one of those places about it.

1 Like

Huh. Surprising. So all the interfaces with addEventListener, including window, aren't actually part of the ECMA spec? That's so fragmented...
Well, it is what it is.
Yep. Even Event is part of the DOM standard, not part of JavaScript.

One thing about the current polyfill is that it doesn't handle successive, synchronous dispatches. So if you were to

myButton.click()
myButton.click()

You'd only see the "Got another click: <event>" message once rather than the expected twice.

Also, a problem for these kinds of dispatches is that because events are mutated as they bubble through the DOM, the fact that the promise is delaying the execution of the code handling the event means the event state could be stale and values like currentTarget may not be what you expect by the time that code gets the event.

for await ( const click of myButton.streamEvents( "click" ) ) {
    console.log( "Got another click: ", click.currentTarget === myButton );
}

// ...

myButton.addEventListener("click", (event) => {
  console.log( "Got a callback click", event.currentTarget === myButton );
});

myButton.click()
// "Got a callback click: true"
// "Got another click: false"

I think it would be challenging to try to get promises to work well with the EventTarget API.

1 Like

You can do that even simpler, see e.g. ecmascript 6 - Pausing javascript async function until user action - Stack Overflow :

EventTarget.prototype.listenForEvent = function(type) {
  return new Promise(resolve => {
    element.addEventListener(type, resolve, {once: true});
  });
};

That's probably a bad idea. An async iterator runs only as fast as it is consumed, but events fire at any rate. It would be quite error-prone to write code like

for await (const event of document.body.streamEvents("scroll")) {
    await delay(50);
    console.log("Debounced scrolling: ", event);
}
console.log("Done"); // dead code?

since the async iterator would not be able to handle the backpressure. Should it just drop events? Should it buffer them? Neither is really desirable.
Using for await leads to users writing sequential code, addEventListener allows writing properly concurrent asynchronous code.

Btw, there is (do I need to say: was?) GitHub - tc39/proposal-observable: Observables for ECMAScript which proposed a proper abstraction for push-based event sources.

1 Like

I think it would be challenging to try to get promises to work well with the EventTarget API.

And also @bergus my thought process was that it wouldn't matter. If the user spammed tons of clicks, obviously the page would not behave right.
But that was a very flawed assumption on my part. These problems are exactly what I've encountered trying to use async / await with WebSockets / WebRTC and something similar to this model. Missing events is absolutely unworkable with something like a handshake. :face_exhaling:

It's a moot point since I don't know HTML well enough at all to involve myself in its development. My intuition still tells me it's possible to wield onmessage with async / await. I hope to figure it out eventually.

No, because they don't have anything to do with JavaScript, just the browser. Other JavaScript runtimes like Node, Deno, Bun, don't have these.