[Bug] asynchronous event handling is principally impossible

This issue has been tracked since 2022-08-03.


I tried to evaluate the "async transparency" of _hyperscript within event handlers...and failed. To my current knowledge of JS, it is principally impossible to implement it anyway:

  • JS event handling expects event handlers to run synchronously - only then preventDefault and stop(Immediate)Propagation are guaranteed to work at any time
  • "yielding" from an event handler (to use a term from JS "generators"), e.g., because of an actually async command, does not prevent JS from propagating the event (unless stop(Immediate)Propagation was called before)
  • the halt command at the end of a _hyperscript event handler is therefore completely useless if the code executed before was actually async - but because of "async transparency" the user/programmer probably does not even know what kind of code he/she is running

I have made a small live example to illustrate the problem.

According to my research, in JS, async event handlers (which allow preventDefault and stop(Immediate)Propagation at any time, even after async calls) are principally impossible

And even if this restriction can be circumvented somehow (e.g. by defining propagation/default handling prior to execution) one still has to implement a separate event queuing mechanism in order to keep the original order of incoming events intact - regardless of the duration of any async calls (this is what you tried, but according to my example, the order of events is not preserved)

Not preserving the order of events and/or letting several event handlers run in parallel opens the door for nasty race conditions making programming much more complicated than before. Particularly, as people may not recognize this danger just by simply "reading" _hyperscript code: "...It is very easy to read, making it obvious what a script is doing..." turns out not to be true any more...

rozek wrote this answer on 2022-08-18

Some (rough) ideas for potential solutions for the mentioned problem:

  • forget about halt and let bubbling/default-handling be configured separately - perhaps depending on event filters (which must then be synchronous, though) If you are interested: here is how Svelte handles the behaviour of event handlers
  • alternatively: whenever a scripted event handler turns out to be actually asynchronous, automatically inhibit any further event propagation and default handling - this sounds quite strict but may be the only safe way...


  • move any events that are to be handled by a script into a separate queue (similar to the one you have already). Within that queue, make sure that the original event order is preserved, i.e., no new event must be handled before handling the previous one has finished
  • perhaps introduce a pass command (instead of halt) that explicitly passes an event to any outer elements (you may have to implement this "custom bubbling" yourself and, perhaps, restrict it to triggering scripted event handlers only)
  • perhaps explicitly allow for default handling from within a scripted event handler (e.g., as part of the new pass command) - this could probably be implemented by cloning the original event (but now with option bubbles set to false) and dispatching it again to the original event.target (disadvantage: the original event.target will get the event twice) Setting bubbles to false will keep the event away from any outer elements and prevent infinite loops

Perhaps, these ideas may help you saving _hyperscript as a whole - otherwise, the goals behind _hyperscript (e.g., "async transparency" and "It is very easy to read, making it obvious what a script is doing") will no longer be achieved...

rozek wrote this answer on 2022-08-18

In order to illustrate the potential fixes mentioned above, I've made two small live examples:

  • this one demonstrates how to auto-disable bubbling and default handling when an event handler turns out to be asynchronous
  • and this one demonstrates how to re-dispatch a previously stopped event in order to trigger a default action (without bubbling)

Meanwhile, I've written a third example which demonstrates a simple queue for (potentially) asynchronous events that avoids "race conditions"

Please note, that such a queue exists completely separate from a browser's built-in event handling - that's why it may be necessary to prevent DOM events from being handled by JS if there are (also) scripted handlers for them.

This solution is not ideal but it's the best I can think of right now...

1cg wrote this answer on 2022-08-18

halt doesn't necessarily stop the event handler, you can halt just the event bubbling or prevent default and continue execution:


so you can do this at the start of the script and then have whatever async behavior you want afterwards

if you have a conditional situation, where event propagation is dependent on an async result, the easiest thing would be to trigger a separate event based on that result and listen for that on the parent:

  on click
     fetch /whatever as json
     if the result's whatever is true
       trigger it-was-true

I can imagine detecting when an event handler doesn't explicitly return from execution (which implies async behavior occurred), cancelling the event, and then, when it does finally return, cloning the event and re-dispatching it on the parent.

rozek wrote this answer on 2022-08-18


I have no doubts that people finally find ways to adjust their scripts in order to achieve the intended result

It is that "async transparency" implies that the programmer should not need to keep asynchrony in mind - because otherwise it's no longer "transparent".

I already wrote a number of JS programs myself in which I changed a previously synchronous approach to an asynchronous one (popular example: localStorage becomes localForage) - and actually had to apply an awful lot of changes as the move from "synchronous" to "asynchronous" changed so much...

Event handling with asynchronous handlers is especially difficult as the order of events (any events, not just those of the same type) must be preserved - or the state machine you are implementing may no longer work as intended.

Again, if you continue to claim to be "async transparent", a script should work as intended regardless of whether certain commands are synchronous or not - the situation could even change over time - or depend on a configuration (again, you may use localStorage -> localForage as a practical example)

1cg wrote this answer on 2022-08-18

We are talking about event bubbling here, right?

The hyperscript scripts themselves are async transparent (as best we can do) in that, if you switch from localStorage to localForage it shouldn't require any changes to your code unless you are relying on event bubbling ordering in some way.

I would recommend reading this:


Hyperscript isn't a formal language, it's a front end scripting language, and the goal is to be useful rather than semantically perfect. I get where you are coming from, and I appreciate that perspective, but we have our own on the matter. Event bubbling ordering isn't an area that is important enough to warrant additional implementation complexity, particularly given the simple work arounds available (trigger and listen for a different event).

If it comes up a bunch in real world use cases, we can take a look at handling it, but until then I'm wary of making abstract consistency the enemy of real world simplicity.

rozek wrote this answer on 2022-08-18

No, not just event bubbling (and default actions)

We are also talking about preserving the order in which events are handled in order to keep the behaviour of state machines predictable - in fact, that's the more important aspect...

Addendum: that's an interesting article you have linked to - the fundamental prerequisite, however, might be: "It is important to remember that the initial virus has to be basically good" - and here our opinions might diverge...

1cg wrote this answer on 2022-08-18

Yes, I see what you are saying. In general, hyperscript should behave as intended, but there are certainly cases where you do have to think a bit. We have the async command:


and expression forms, for example, to handle specialized situations:


(frankly, they are a bit of a mess.)

And we have the queue keyword to determine how event handlers behave when they receive events would imply parallel execution:


So, not perfect. But pretty good.

I don't want to discourage you from contributing, however, and I'm willing to continue to consider the issue. Maybe you'd like to jump on the discord, which is probably a better medium for back-and-forth speculative discussions than github?


As and aside, there is an area that I think would be very useful to contribute to hyperscript that might interest you: improving the debugger.

rozek wrote this answer on 2022-08-18


as mentioned somewhere else: I really like the idea of

  • having an xTalk language that manipulates the DOM (and has syntactic sugar for that)
  • with direct JavaScript interoperability and
  • "async transparency" (JS is so horrible in that aspect)

and spent some time evaluating it (e.g., to see if I could build a visual IDE around it - s.th. like _hypercard). As you may see from the number of issues I opened, there were quite a number of situations that "surprised" me and consumed a lot of time...

For the time being, I'll have to put my activities on hold for a while and watch how _hyperscript evolves - perhaps experimenting with it again at some time in the future.

In any case, I wish you every success!

Addendum: just in case that I wasn't explicit enough: I am primarily interested in using _hyperscript, not so much in building it. Of course, I could enhance it here and there, if that does not consume too much time - but, actually, I prefer building applications with _hyperscript

More Details About Repo
Owner Name bigskysoftware
Repo Name _hyperscript
Full Name bigskysoftware/_hyperscript
Language JavaScript
Created Date 2020-05-27
Updated Date 2022-11-25
Star Count 1265
Watcher Count 18
Fork Count 75
Issue Count 86


Issue Title Created Date Updated Date