This example provided to explain tasks vs microtasks doesn’t make sense to me. Shouldn’t the event listener be executed after the current task has finished, so “Data fetched” should get logged before “Loaded data” in the case of it being cached? And in the case of not being cached it’ll be the same because the microtask gets run after the initial task. What am I missing?
https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide#
‘One situation in which microtasks can be used to ensure that the ordering of execution is always consistent is when promises are used in one clause of an if…else statement (or other conditional statement), but not in the other clause. Consider code such as this:
customElement.prototype.getData = (url) => {
if (this.cache[url]) {
this.data = this.cache[url];
this.dispatchEvent(new Event("load"));
} else {
fetch(url)
.then((result) => result.arrayBuffer())
.then((data) => {
this.cache[url] = data;
this.data = data;
this.dispatchEvent(new Event("load"));
});
}
};
The problem introduced here is that by using a task in one branch of the if…else statement (in the case in which the image is available in the cache) but having promises involved in the else clause, we have a situation in which the order of operations can vary; for example, as seen below.
element.addEventListener("load", () => console.log("Loaded data"));
console.log("Fetching data…");
element.getData();
console.log("Data fetched");
Executing this code twice in a row gives the following results.
When the data is not cached:
Fetching data
Data fetched
Loaded data
When the data is cached:
Fetching data
Loaded data
Data fetched
…‘
2
Answers
The example from MDN… maybe assumes the reader knows too much?
The problem that the example is trying to highlight is that the
getData
function is synchronous in one branch and asynchronous in the other, and this leads to bugs. Consider that the caller ofgetData
doesn’t understand they need an event listener and makes some assumptions about whether the.data
property is populated:in the cached case, this code works fine. In the fetch case, it blows up with a runtime error. But there are even more pernicious problems:
Here we have potential for even more subtle bugs, because now in the fetch case the call to
someAsyncFunctionThatReliesOnElementData
might still succeed if it takes long enough that the data has come back (since it’s async). In other words, we’ve now introduced a potential race condition on top of the other problems.Now, all of this goes away if the caller just listens for the element’s
load
event like they’re supposed to, but the point is that by queueing the microtask in the cache case that you’ve eliminated an entire class of misuse of the API: anyone who assumes that it synchronously sets the.data
property will get a fast failure instead of an intermittent one.I think part of the problem is that this whole thing is horribly contrived. There are better ways to write something like this, included but probably not limited to removing the
data
property entirely and:In other words, the fix here is to refactor the element’s API rather than muck around with
queueMicrotask
.Posting this because it’s easier than commenting: now that we have
async
functions, I think that first example function would me made much clearer by being written that way:(Note that another issue with that example was that the function should not be an
=>
function, becausethis
won’t work.)Now in the
if
there’s (effectively) one Promise resolution and in theelse
there are two, but the problem mentioned in that article is basically "solved". Calling.getData()
now (withawait
):Kind of skirts the whole issue, because that’s what
await
does. In the synchronousif
branch, the update and event firing are immediate, but the calling environment will still wait for the implicit Promise to resolve.I think a bug should be logged on the MDN page because it certainly would be possible to come up with a less contorted example.