Asynchronous opererations

20 views
Skip to first unread message

Alan Karp

unread,
Mar 7, 2024, 1:31:25 PMMar 7
to <friam@googlegroups.com>
Last week I described some issues I've had with asynchronous operations in my extension.  Doug (I think) suggested I write up my thoughts, so I did. 

https://github.com/alanhkarp/SitePassword/blob/PBKDF2/3async.pdf.

I have a few questions for you folks.

What did I get wrong?
Does what I propose make sense?
Is there a better way to do it?

--------------
Alan Karp

Mike Stay

unread,
Mar 7, 2024, 4:42:24 PMMar 7
to fr...@googlegroups.com
chrome.runtime.sendMessage returns a promise if you don't provide a callback:
https://developer.chrome.com/docs/extensions/reference/api/runtime#method-sendMessage
> --
> You received this message because you are subscribed to the Google Groups "friam" group.
> To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.
> To view this discussion on the web visit https://groups.google.com/d/msgid/friam/CANpA1Z2nYzXd8ioE9Oa9KynYBO_EW7hZMZJs-kJ-%2BeBzEFYfTQ%40mail.gmail.com.



--
Mike Stay - meta...@gmail.com
https://math.ucr.edu/~mike
https://reperiendi.wordpress.com

Alan Karp

unread,
Mar 7, 2024, 7:45:16 PMMar 7
to fr...@googlegroups.com
Thanks, Mike.  I don't know how I missed that, but I'm going to have a big negative code day.

I just learned that Firefox recommends a polyfill to wrap those calls, which makes me wonder how long the Chrome API has been returning promises.

--------------
Alan Karp


Mike Stay

unread,
Mar 8, 2024, 11:49:48 AMMar 8
to fr...@googlegroups.com
On Thu, Mar 7, 2024 at 5:45 PM Alan Karp <alan...@gmail.com> wrote:
> I just learned that Firefox recommends a polyfill to wrap those calls, which makes me wonder how long the Chrome API has been returning promises.

Where did you see that? According to MDN, Firefox doesn't have the
callback option; it only returns promises:
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/runtime/sendMessage

Chris Hibbert

unread,
Mar 8, 2024, 11:53:08 AMMar 8
to fr...@googlegroups.com
On 3/7/24 10:31 AM, Alan Karp wrote:
>
> What did I get wrong?
> Does what I propose make sense?
> Is there a better way to do it?

MarkM has convinced us at Agoric that this form is usually a bad idea:

let result;
if (condition) {
result = await foo();
} else {
result = "";
}

(if there are no other awaits in this fn) the caller can't be sure
whether the code will return synchronously or not. For a while our
linting rules forbade non-top-level awaits for this reason. After
pushback and discussion, other voices convinced us all that the correct
rule is that a non-top-level await (i.e. await in a loop or conditional)
can't be the first await in a function, and that's what we enforce now.

This means that if you can't come up with a better alternative, you can
precede the above with `await null;` and pass the linter. The effect is
that callers will always get a promise from this function.

I found the rest of the doc's focus on controlling the order in which
events occur to be jarring. I'll admit that I don't write much UI code,
but one of the things I've learned from writing in JavaScript is to
accept that async functions will return results "later". I write a lot
of .then() clauses (though we have a different syntax for them) and
expect things to be taken care of eventually. This can make testing
difficult, but in a distributed system, you can't expect local immediate
actions. In tests, I often make mocks that allow me to control the
timing of responses.

We've recently added tools that provide for durable handling of
promises, which means it's possible to write long-lived services that
rely on other long-lived services, and expect to survive a restart or
upgrade of either the code you're writing or code you're depending on.
It's quite powerful.

Chris
--
The government's efforts to expand "access" to care while limiting
costs are like blowing up a balloon while simultaneously squeezing
it. The balloon continues to inflate, but in misshapen form.
---David Goldhill

Chris Hibbert
hib...@mydruthers.com
Blog: http://www.pancrit.org
http://mydruthers.com



Kevin Reid

unread,
Mar 8, 2024, 1:23:15 PMMar 8
to fr...@googlegroups.com
On Fri, Mar 8, 2024 at 8:53 AM Chris Hibbert <hib...@mydruthers.com> wrote:
... (if there are no other awaits in this fn) the caller can't be sure whether the code will return synchronously or not. For a while our linting rules forbade non-top-level awaits for this reason. After pushback and discussion, other voices convinced us all that the correct rule is that a non-top-level await (i.e. await in a loop or conditional) can't be the first await in a function, and that's what we enforce now.

Rust's async syntax got this part right (at least in part due to other technical considerations regarding value storage and movement): the entire body of an `async fn` is executed only later. It's still possible for a function to have immediate effects, but it has to choose that by avoiding the `async` sugar.

Alan Karp

unread,
Mar 8, 2024, 3:50:13 PMMar 8
to fr...@googlegroups.com
I don't understand that.  I run the exact same code with callbacks on both Chrome and Firefox.

I found out about the polyfill from https://github.com/mozilla/webextension-polyfill.

--------------
Alan Karp


--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Alan Karp

unread,
Mar 8, 2024, 4:40:28 PMMar 8
to fr...@googlegroups.com
Comments inline.

--------------
Alan Karp


On Fri, Mar 8, 2024 at 8:53 AM Chris Hibbert <hib...@mydruthers.com> wrote:

MarkM has convinced us at Agoric that this form is usually a bad idea:

let result;
if (condition) {
   result = await foo();
} else {
   result = "";
}

(if there are no other awaits in this fn) the caller can't be sure
whether the code will return synchronously or not. For a while our
linting rules forbade non-top-level awaits for this reason. After
pushback and discussion, other voices convinced us all that the correct
rule is that a non-top-level await (i.e. await in a loop or conditional)
can't be the first await in a function, and that's what we enforce now.

Not a bad policy, but the fact that you need it reflects poorly on the tooling.  I can't await foo unless foo returns a promise, so I shouldn't be able to ignore the return value of an async function.  If that violates the spirit of JavaScript, the IDE should at least warn you.  VSCode does not.  

This means that if you can't come up with a better alternative, you can
precede the above with `await null;` and pass the linter. The effect is
that callers will always get a promise from this function.

I like that as a form of documenting the function's behavior. 

I found the rest of the doc's focus on controlling the order in which
events occur to be jarring. I'll admit that I don't write much UI code,
but one of the things I've learned from writing in JavaScript is to
accept that async functions will return results "later". I write a lot
of .then() clauses (though we have a different syntax for them) and
expect things to be taken care of eventually. This can make testing
difficult, but in a distributed system, you can't expect local immediate
actions. In tests, I often make mocks that allow me to control the
timing of responses.

In general, you can't depend on the delivery order of uncorrelated events.  However, you often have to impose an order on how they are processed.  That's what I need to do.  For example, if one user generated event updates an element, the user would be quite surprised if a subsequent event did not reflect that update.  You can usually rely on the fact that people are too slow to generate such anomalies, but that's not always the case.  It certainly doesn't apply to UI testing.

Event handlers are tricky.  They take no arguments, return no values, and return promptly, which caused me some headaches.  For one example, a blur event on one element invokes an async function that updates another one.  My test would often check the value of the updated element before the update finished.  My initial solution was to use setTimeout to delay checking, but I found await on a promise in the event handler works better.   


We've recently added tools that provide for durable handling of
promises, which means it's possible to write long-lived services that
rely on other long-lived services, and expect to survive a restart or
upgrade of either the code you're writing or code you're depending on.
It's quite powerful.

I'm very glad that I don't have that problem. 

Chris
--
The government's efforts to expand "access" to care while limiting
costs are like blowing up a balloon while simultaneously squeezing
it.  The balloon continues to inflate, but in misshapen form.
     ---David Goldhill

Chris Hibbert
hib...@mydruthers.com
Blog:   http://www.pancrit.org
http://mydruthers.com



--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Chip Morningstar

unread,
Mar 8, 2024, 6:04:42 PMMar 8
to 'Kevin Reid' via friam

On Mar 8, 2024, at 1:40 PM, Alan Karp <alan...@gmail.com> wrote:


On Fri, Mar 8, 2024 at 8:53 AM Chris Hibbert <hib...@mydruthers.com> wrote:

MarkM has convinced us at Agoric that this form is usually a bad idea:

let result;
if (condition) {
   result = await foo();
} else {
   result = "";
}

(if there are no other awaits in this fn) the caller can't be sure 
whether the code will return synchronously or not. For a while our 
linting rules forbade non-top-level awaits for this reason. After 
pushback and discussion, other voices convinced us all that the correct 
rule is that a non-top-level await (i.e. await in a loop or conditional) 
can't be the first await in a function, and that's what we enforce now.

Not a bad policy, but the fact that you need it reflects poorly on the tooling.  I can't await foo unless foo returns a promise, so I shouldn't be able to ignore the return value of an async function.  If that violates the spirit of JavaScript, the IDE should at least warn you.  VSCode does not.  

This means that if you can't come up with a better alternative, you can 
precede the above with `await null;` and pass the linter. The effect is 
that callers will always get a promise from this function.

I think this is not quite right.  If the function is declared `async` (which it must be in order to be allowed to use `await`), the function will *always* return a promise.

As I understand it, the issue is that if you have two (or more) execution paths, one of which passes through an `await` and the other of which does not, then you can have code inside the function that might or might not execute within the same turn as the caller depending on conditions.  This means that if the code that lexically follows the `await` side effects the environment or is sensitive to the state of the calling environment, then it might or might not have behavior that's entangled with its caller in subtle and surprising ways.  With respect to any particular fragment of code you can, in fact, generally do a sufficiently rigorous analysis that will tell you whether or not this is a problem in that specific case (and, in fact, it's almost never a problem, which is terrible because that makes it easy to become lazy).  However, this analysis is tricky and easy to get wrong.  Inserting something like `await null;` will force any code that follows it into a separate turn unconditionally and thereby spare you the pain.

— Chip

Alan Karp

unread,
Mar 11, 2024, 12:57:50 PMMar 11
to fr...@googlegroups.com
On Fri, Mar 8, 2024 at 3:04 PM Chip Morningstar <ch...@fudco.com> wrote:
Inserting something like `await null;` will force any code that follows it into a separate turn unconditionally and thereby spare you the pain.
That's what I thought, but a simple experiment makes me question that.

A paste operation does not update the element in the current turn.  The recommended approach is to use setTimeout.  (It's the blur event handler on the domainname element that does the desired computation.)

$domainname.onpaste = function () {
$domainnamemenuforget.style.opacity = "1";
setTimeout(() => {
enableBookmark();
$bookmark.focus(); // NOTE: this triggers `onblur` on $domainname
}, 0);
}

I thought that I would get the same result with a promise.

$domainname.onpaste = async function () {
$domainnamemenuforget.style.opacity = "1";
await new Promise((resolve) => {
enableBookmark();
$bookmark.focus(); // NOTE: this triggers `onblur` on $domainname
resolve();
});
}
 
but that's not the case.  I set a breakpoint on the line with $bookmark.focus().  With setTimeout, the $domainname element shows the text I pasted.  With the promise version, that field is blank; the text I pasted only shows up later.

Copilot says that the body of the promise is put on the microtask queue and runs before any setTimeouts on the task queue.  It appears that tasks on that queue are executed as part of the current macrotask turn, at least as far as DOM updates are concerned.

All that being said, I will follow Chip's advice and put "await null" on the alternate branch to avoid the kind of confusion he warns about.

--------------
Alan Karp


--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Alan Karp

unread,
Mar 11, 2024, 1:37:06 PMMar 11
to fr...@googlegroups.com
Further information on Chip's suggestion to use "await null" to avoid certain sequencing errors.

I inserted "await null" in an else-branch.  VSCode says that the await has no effect on this code.  Copilot recommends "await Promise.resolve()" instead.

--------------
Alan Karp


On Fri, Mar 8, 2024 at 3:04 PM Chip Morningstar <ch...@fudco.com> wrote:
--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Mark S. Miller

unread,
Mar 11, 2024, 1:47:14 PMMar 11
to fr...@googlegroups.com
const f = async () => {
console.log('a');
await null;
console.log('c');
};

const g = () => {
f();
console.log('b');
}

g();


prints

a
b
c

Proving that vscode is wrong, and `await null;` is a turn interleaving point.

Mark S. Miller

unread,
Mar 11, 2024, 1:49:27 PMMar 11
to fr...@googlegroups.com
Also, vscode says nothing to me about this code. I am not using copilot. Is it copilot that's giving you this warning? Anyone seeing this warning without copilot? Anyone else with copilot?

--
  Cheers,
  --MarkM

Mark S. Miller

unread,
Mar 11, 2024, 1:58:10 PMMar 11
to fr...@googlegroups.com
On putting it into one branch of a conditional:

const f = async flag => {
console.log('a');
if (flag) {
await null;
}
console.log('c');
};

const g = flag => {
f(flag);
console.log('b');
}

g(true) produces a b c as before
g(false) produces a c b

Mark S. Miller

unread,
Mar 11, 2024, 1:59:24 PMMar 11
to fr...@googlegroups.com
Some close curlies became invisible in the syntax highlighting as pasted into email. The actual text is fine.

Mike Stay

unread,
Mar 11, 2024, 3:25:42 PMMar 11
to fr...@googlegroups.com
On Mon, Mar 11, 2024 at 10:57 AM Alan Karp <alan...@gmail.com> wrote:
> Copilot says that the body of the promise is put on the microtask queue and runs before any setTimeouts on the task queue. It appears that tasks on that queue are executed as part of the current macrotask turn, at least as far as DOM updates are concerned.

https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide

Mark S. Miller

unread,
Mar 11, 2024, 5:56:33 PMMar 11
to fr...@googlegroups.com
Yes. setTimeout is on a strictly lower priority queue than the promise queue.

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Chris Hibbert

unread,
Mar 11, 2024, 11:27:42 PMMar 11
to fr...@googlegroups.com
On 3/8/24 3:04 PM, Chip Morningstar wrote:
>>
>
> I think this is not quite right.  If the function is declared `async`
> (which it must be in order to be allowed to use `await`), the function
> will *always* return a promise.

You are, of course, right.

> As I understand it, the issue is that if you have two (or more)
> execution paths, one of which passes through an `await` and the other of
> which does not, then you can have code inside the function that might or
> might not execute within the same turn as the caller depending on conditions.

Yeah. The term we use is the "synchronous prelude". I don't know whether
this is official JS terminology, or something we only use in-house (you
probably know better than I, Chip.) It refers to and highlights the fact
that when you call an async function in JavaScript, control immediately
passes to that function, and code there runs until there's an await (or
equivalent?). If there are awaits inside conditionals, there can be
wildly varying amounts of code that execute there on different calls,
which is why our style prevents that.

The caller gets a promise for the return result, but may be surprised
that the synchronous prelude runs before control is returned to the caller.

MarkM's example shows what this looks like.

Alan Karp

unread,
Mar 12, 2024, 12:48:41 PMMar 12
to fr...@googlegroups.com
On Fri, Mar 8, 2024 at 3:04 PM Chip Morningstar <ch...@fudco.com> wrote:
>
> As I understand it, the issue is that if you have two (or more) execution paths, one of 
> which passes through an `await` and the other of which does not, then you can have 
> code inside the function that might or might not execute within the same turn as the 
> caller depending on conditions.
>
While that's certainly true when using .then, is it true with await?

Consider

if (condition) {
    result = ... // 1
} else {
    asyncfn().then((result) => {
        ... // 2
    });
}
... // 3


In this case, the order is 1, 3 in the same microturn if the condition is true and 3, 2 in different microturns if it is false. The hazard Chip points out can occur.  (I believe that's why I was seeing some odd behavior before changing to the next pattern.)

The way to avoid that problem is to create a continuation.

if (condition) {
    result = ... // 1
    remainder(result)
} else {
    asyncfn().then((result) => {
        ... // 2
        remainder(result)
    });
}
function remainder(result) {
    ... // 3
}


The order is then 1,3 in the same microturn for true and 2, 3 in the same microturn for false, which avoids the hazard.

An alternative is

if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3


I believe the sequence is 1, 3 in the same microturn if the condition is true and 2, 3 in the same microturn if it is false.  The hazard Chip points out cannot occur.

Did I get that right?

--------------
Alan Karp

Mark S. Miller

unread,
Mar 12, 2024, 1:23:44 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 9:48 AM Alan Karp <alan...@gmail.com> wrote:
On Fri, Mar 8, 2024 at 3:04 PM Chip Morningstar <ch...@fudco.com> wrote:
>
> As I understand it, the issue is that if you have two (or more) execution paths, one of 
> which passes through an `await` and the other of which does not, then you can have 
> code inside the function that might or might not execute within the same turn as the 
> caller depending on conditions.
>
While that's certainly true when using .then, is it true with await?

Consider

if (condition) {
    result = ... // 1
} else {
    asyncfn().then((result) => {
        ... // 2
    });
}
... // 3


In this case, the order is 1, 3 in the same microturn if the condition is true and 3, 2 in different microturns if it is false. The hazard Chip points out can occur.  (I believe that's why I was seeing some odd behavior before changing to the next pattern.)

There's no such thing as a "microturn". You mean "turn". 
 

The way to avoid that problem is to create a continuation.

if (condition) {
    result = ... // 1
    remainder(result)
} else {
    asyncfn().then((result) => {
        ... // 2
        remainder(result)
    });
}
function remainder(result) {
    ... // 3
}


The order is then 1,3 in the same microturn for true and 2, 3 in the same microturn for false, which avoids the hazard.

An alternative is

if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3


I believe the sequence is 1, 3 in the same microturn if the condition is true and 2, 3 in the same microturn if it is false.  The hazard Chip points out cannot occur.

Did I get that right?

No. Put a "// 0" before the if. Now 0 and 3 are either in the same of different turns depending on condition.

// 0
if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3


 

--------------
Alan Karp

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Alan Karp

unread,
Mar 12, 2024, 2:27:24 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 10:23 AM Mark S. Miller <eri...@gmail.com> wrote:

There's no such thing as a "microturn". You mean "turn". 

I'm thinking of the behavior of paste.  The body of a setTimeout runs after the DOM update.  That's clearly the next turn.  The body of a promise runs before the DOM update but after other code.  I made up the word "microturn" to make that distinction. 
 
An alternative is

if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3


I believe the sequence is 1, 3 in the same microturn if the condition is true and 2, 3 in the same microturn if it is false.  The hazard Chip points out cannot occur.

Did I get that right?

No. Put a "// 0" before the if. Now 0 and 3 are either in the same of different turns depending on condition.

// 0
if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3

True, but does that exhibit the hazard that Chip pointed out?  Does it create any other hazards?  The answer would be yes if the behavior was like setTimeout because the two cases might see different DOM states.  Of course, the safe thing to do is to add an "await null" to the other branch, so I'm just asking out of general interest..

--------------
Alan Karp

Mark S. Miller

unread,
Mar 12, 2024, 2:37:51 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 11:27 AM Alan Karp <alan...@gmail.com> wrote:
On Tue, Mar 12, 2024 at 10:23 AM Mark S. Miller <eri...@gmail.com> wrote:

There's no such thing as a "microturn". You mean "turn". 

I'm thinking of the behavior of paste.  The body of a setTimeout runs after the DOM update.  That's clearly the next turn.  The body of a promise runs before the DOM update but after other code.  I made up the word "microturn" to make that distinction. 

No, the unit of servicing the low priority queue is not a turn, by definition. A turn is the interval between an empty stack and an empty stack in an event-loop system. This can only be the unit of servicing the highest priority queue.

What you're referring to is a browser concept, not an event-loop concept. Browser terminology doesn't use the term "turn" anyway.
 
 
An alternative is

if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3


I believe the sequence is 1, 3 in the same microturn if the condition is true and 2, 3 in the same microturn if it is false.  The hazard Chip points out cannot occur.

Did I get that right?

No. Put a "// 0" before the if. Now 0 and 3 are either in the same of different turns depending on condition.

// 0
if (condition) {
    result = ... // 1
} else {
    result = await asyncfn();
    ... // 2
}
... // 3

True, but does that exhibit the hazard that Chip pointed out?  Does it create any other hazards?  The answer would be yes if the behavior was like setTimeout because the two cases might see different DOM states.  Of course, the safe thing to do is to add an "await null" to the other branch, so I'm just asking out of general interest..

Actually, we don't need the "// 0" to explain the hazard. The problem is that there is no unambiguous synchronous prelude. If condition, then 3 happens before returning to caller. Else 3 happens in a separate turn after returning to caller.

If the whole thing is preceded by an "await null;" outside the condition at top level, then 3 always happens in a separate turn after the caller has completed.

 

--------------
Alan Karp

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Raoul Duke

unread,
Mar 12, 2024, 4:26:53 PMMar 12
to fr...@googlegroups.com
task vs. microtask in browsers. "The microtask queue has been around
for a while, but it's historically been used only internally in order
to drive things like promises."
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide/In_depth
in case this happens to be educational somewhere to somebody. :-)

Mike Stay

unread,
Mar 12, 2024, 4:54:35 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 12:37 PM Mark S. Miller <eri...@gmail.com> wrote:
> On Tue, Mar 12, 2024 at 11:27 AM Alan Karp <alan...@gmail.com> wrote:
>> On Tue, Mar 12, 2024 at 10:23 AM Mark S. Miller <eri...@gmail.com> wrote:
>>>
>>> There's no such thing as a "microturn". You mean "turn".
>>
>> I'm thinking of the behavior of paste. The body of a setTimeout runs after the DOM update. That's clearly the next turn. The body of a promise runs before the DOM update but after other code. I made up the word "microturn" to make that distinction.
>
> No, the unit of servicing the low priority queue is not a turn, by definition. A turn is the interval between an empty stack and an empty stack in an event-loop system. This can only be the unit of servicing the highest priority queue.

Alan, here's a more apt definition of "microturn": turns contain
microturns. The turn pops an event off of the event queue and invokes
the event handler, which takes the first microturn. The event handler
may queue microtasks in the microtask queue and events in the event
queue. When the event handler is done, the execution stack is empty,
but microtasks remain in the microtask queue. Handling a microtask
takes a microturn, in this terminology. Once the microtask queue is
empty, the turn ends and the browser moves on to the next event in the
event queue.

> if (condition) {
> result = ... // 1
> } else {
> result = await asyncfn();
> ... // 2
> }
> ... // 3

Here if the condition flag is set, 1 and 3 happen in the first
microturn, the event handler. If the condition flag is unset and
asyncfn only uses promises, then 3 happens in the first microturn and
2 happens in a later microturn, but both are handled in the same turn.
If the condition flag is not set and asyncfn uses something like
setTimeout or fetch that wait on an event, 3 happens in the first
microturn and the turn ends, then 1 happens in a future turn.

> If the whole thing is preceded by an "await null;" outside the condition at top level, then 3 always happens in a separate turn after the caller has completed.

In this terminology, preceding by "await null;" means 3 always happens
in a future microturn after the caller has completed, but it will
happen in the same turn if the condition flag is set and a different
turn otherwise.

To make sure that 3 always happens in another turn, use
await new Promise((resolve)=>setTimeout(resolve,0));

Alan Karp

unread,
Mar 12, 2024, 5:07:08 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 11:37 AM Mark S. Miller <eri...@gmail.com> wrote:


On Tue, Mar 12, 2024 at 11:27 AM Alan Karp <alan...@gmail.com> wrote:
On Tue, Mar 12, 2024 at 10:23 AM Mark S. Miller <eri...@gmail.com> wrote:

There's no such thing as a "microturn". You mean "turn". 

I'm thinking of the behavior of paste.  The body of a setTimeout runs after the DOM update.  That's clearly the next turn.  The body of a promise runs before the DOM update but after other code.  I made up the word "microturn" to make that distinction. 

No, the unit of servicing the low priority queue is not a turn, by definition. A turn is the interval between an empty stack and an empty stack in an event-loop system. This can only be the unit of servicing the highest priority queue.

What you're referring to is a browser concept, not an event-loop concept. Browser terminology doesn't use the term "turn" anyway.

I'm working in a browser context, so the difference between something finishing before the DOM update and after is significant.  Is there terminology to explain that difference? 

--------------
Alan Karp

Alan Karp

unread,
Mar 12, 2024, 5:12:56 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 1:54 PM Mike Stay <meta...@gmail.com> wrote:

Alan, here's a more apt definition of "microturn": turns contain
microturns.  The turn pops an event off of the event queue and invokes
the event handler, which takes the first microturn.  The event handler
may queue microtasks in the microtask queue and events in the event
queue.  When the event handler is done, the execution stack is empty,
but microtasks remain in the microtask queue.  Handling a microtask
takes a microturn, in this terminology.  Once the microtask queue is
empty, the turn ends and the browser moves on to the next event in the
event queue.

Thanks for the explanation. 

> If the whole thing is preceded by an "await null;" outside the condition at top level, then 3 always happens in a separate turn after the caller has completed.

In this terminology, preceding by "await null;" means 3 always happens
in a future microturn after the caller has completed, but it will
happen in the same turn if the condition flag is set and a different
turn otherwise.

To make sure that 3 always happens in another turn, use
   await new Promise((resolve)=>setTimeout(resolve,0));

That would be far too slow given that 0 really means 4 ms.  Fortunately, there are only a few DOM changes that don't appear until the next turn, and I'm already using your proposal for them.

--------------
Alan Karp

Mark S. Miller

unread,
Mar 12, 2024, 6:06:58 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 1:54 PM Mike Stay <meta...@gmail.com> wrote:
On Tue, Mar 12, 2024 at 12:37 PM Mark S. Miller <eri...@gmail.com> wrote:
> On Tue, Mar 12, 2024 at 11:27 AM Alan Karp <alan...@gmail.com> wrote:
>> On Tue, Mar 12, 2024 at 10:23 AM Mark S. Miller <eri...@gmail.com> wrote:
>>>
>>> There's no such thing as a "microturn". You mean "turn".
>>
>> I'm thinking of the behavior of paste.  The body of a setTimeout runs after the DOM update.  That's clearly the next turn.  The body of a promise runs before the DOM update but after other code.  I made up the word "microturn" to make that distinction.
>
> No, the unit of servicing the low priority queue is not a turn, by definition. A turn is the interval between an empty stack and an empty stack in an event-loop system. This can only be the unit of servicing the highest priority queue.

Alan, here's a more apt definition of "microturn": turns contain
microturns.  The turn pops an event off of the event queue and invokes
the event handler, which takes the first microturn.  The event handler
may queue microtasks in the microtask queue and events in the event
queue.  When the event handler is done, the execution stack is empty,
but microtasks remain in the microtask queue.  Handling a microtask
takes a microturn, in this terminology.  Once the microtask queue is
empty, the turn ends and the browser moves on to the next event in the
event queue.

Those are fine definitions of other words. But not of "turn" or "microturn". There is no such thing as a microturn. The thing you're calling a "microturn" is a turn. The thing you're calling a "turn" is a browser concept that has browser terminology. It is not an event-loop concept and has no "turn" oriented term. I think the browser concept corresponce is

Browser microtask -> turn
Browser task (or macrotask) -> no corresponding term needed

 

> if (condition) {
>     result = ... // 1
> } else {
>     result = await asyncfn();
>     ... // 2
> }
> ... // 3

Here if the condition flag is set, 1 and 3 happen in the first
microturn, the event handler.  If the condition flag is unset and
asyncfn only uses promises, then 3 happens in the first microturn and
2 happens in a later microturn, but both are handled in the same turn.
If the condition flag is not set and asyncfn uses something like
setTimeout or fetch that wait on an event, 3 happens in the first
microturn and the turn ends, then 1 happens in a future turn.

> If the whole thing is preceded by an "await null;" outside the condition at top level, then 3 always happens in a separate turn after the caller has completed.

In this terminology, preceding by "await null;" means 3 always happens
in a future microturn after the caller has completed, but it will
happen in the same turn if the condition flag is set and a different
turn otherwise.

To make sure that 3 always happens in another turn, use
   await new Promise((resolve)=>setTimeout(resolve,0));
--
Mike Stay - meta...@gmail.com
https://math.ucr.edu/~mike
https://reperiendi.wordpress.com

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Mark S. Miller

unread,
Mar 12, 2024, 6:07:56 PMMar 12
to fr...@googlegroups.com
Task vs Microtask. Leave turns out of it.
 

--------------
Alan Karp

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Mike Stay

unread,
Mar 12, 2024, 6:51:05 PMMar 12
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 4:07 PM 'Mark S. Miller' via friam
<fr...@googlegroups.com> wrote:
> Task vs Microtask. Leave turns out of it.

The ECMAScript standard talks about "jobs". As far as I can tell,
they are exactly the same as turns. The standard allows them to be
scheduled with different priorities, which is probably a better way to
look at it. In the browser, microtask jobs are higher priority than
event jobs.

https://tc39.es/ecma262/#sec-jobs-and-job-queues

Updating what I wrote before to use the job terminology:

> > if (condition) {
> > result = ... // 1
> > } else {
> > result = await asyncfn();
> > ... // 2
> > }
> > ... // 3
>
> Here if the condition flag is set, 1 and 3 happen in the first
> job, the event handler. If the condition flag is unset and
> asyncfn only uses promises, then 3 happens in the first job and
> 2 happens in a later job, but both are handled before the next event handler job.
> If the condition flag is not set and asyncfn uses something like
> setTimeout or fetch that wait on an event, 3 happens in the first
> job, then 1 happens in a future event job.
>
> > If the whole thing is preceded by an "await null;" outside the condition at top level, then 3 always happens in a separate turn after the caller has completed.
>
> In this terminology, preceding by "await null;" means 3 always happens
> in a future job after the caller has completed, but it will
> happen before the next event job if the condition flag is set and after
> all microtask jobs otherwise.

Mark S. Miller

unread,
Mar 12, 2024, 6:56:02 PMMar 12
to fr...@googlegroups.com
Thanks.

--
You received this message because you are subscribed to the Google Groups "friam" group.
To unsubscribe from this group and stop receiving emails from it, send an email to friam+un...@googlegroups.com.

Mike Stay

unread,
Mar 13, 2024, 1:59:42 AMMar 13
to fr...@googlegroups.com
On Tue, Mar 12, 2024 at 4:50 PM Mike Stay <meta...@gmail.com> wrote:
> Updating what I wrote before to use the job terminology:
>
> > > if (condition) {
> > > result = ... // 1
> > > } else {
> > > result = await asyncfn();
> > > ... // 2
> > > }
> > > ... // 3
> >
> > Here if the condition flag is set, 1 and 3 happen in the first
> > job, the event handler. If the condition flag is unset and
> > asyncfn only uses promises, then 3 happens in the first job and
> > 2 happens in a later job, but both are handled before the next event handler job.
> > If the condition flag is not set and asyncfn uses something like
> > setTimeout or fetch that wait on an event, 3 happens in the first
> > job, then 1 happens in a future event job.

Oops! In that last line, 1 should be 2.
Reply all
Reply to author
Forward
0 new messages