For example I'm doing a RegExp replacement on a body of text:
text = text.replace(re, function (match, comment, op1, op2, op3, op4, op5, op6, op7) {
if (op1 !== undefined) {
log("processing @@");
parseEnum(config, op1, args1);
} else if (op2 !== undefined) {
log("processing {{}}");
parseConfig(config, interpolateConfig(config, op2));
return "";
} else if (op3 !== undefined && !configOnly) {
log("processing [[]]");
return await parseMacro(config, op3, asyncInstances); // <---------- HERE
} else if (op4 !== undefined) {
log("processing <<>>");
await parseInclude(config, interpolateConfig(config, op4)); // <---- HERE
return "";
} else if (op5 !== undefined && !configOnly) {
log("processing ~~~~");
// TODO parseStyle(op5);
} else if (op6 !== undefined) {
log("processing $$");
return interpolateConfig(config, op6);
} else if (op7 !== undefined) {
log("processing ^^");
}
});
And I need these replacements to occur synchronously, i.e. each call to the replacement function must complete before the next match and replacement. However, depending on the match, sometimes I have to call an async function. So to keep things synchronous, I decided to use await. But to do that, I also had to change the anonymous function to async:
text = text.replace(re, async function (match, comment, op1, op2, op3, op4, op5, op6, op7) {
++++++
Unfortunately however, String.replace doesn't accept an async function. I found in this answer a utility for doing just that, copied verbatim from that post by @ChrisMorgan:
async function replaceAsync(string, regexp, replacerFunction) {
const replacements = await Promise.all(
Array.from(string.matchAll(regexp),
match => replacerFunction(...match)));
let i = 0;
return string.replace(regexp, () => replacements[i++]);
}
Using this like this (omitting code that hasn't changed)—
text = await replaceAsync(
text,
re,
async function (match, op1, args1, op2, op3, op4, op5, op6, op7) {
...
}
);
—it actually seemed to work. I thought that the replacements would still occur synchronously—well maybe the better word here is sequentially—because underneath the hood we're still calling String.replace one at a time. But later I began seeing strange results and realized I was mistaken. The replacement function was being called in parallel, and "future" replacements started processing whenever the current replacement gave up control (e.g. to fetch a file, etc. which is the reason some functions are async).
The only way I can see around this right now is to avoid async/await altogether and use Promise.resolve() instead. But I was trying to use async/await everywhere to keep a consistent style. Is there another way to do this using async/await that I'm not seeing?
Update: Hm, I cannot figure out how to solve this using Promise.resolve() either. I thought that that would essentially act like await but it does not.
To address the title and… theme: there’s no general way to do this in typical JavaScript. Async/await/promises are concerned with control flow, and you can’t insert accounting for control flow into something that didn’t already have it.
Specifically, in this case, String.prototype.replace
can’t do async operations, and the above is why you had to find a specific implementation of replaceAsync
. The particular behavior with respect to control flow, then, is down to the specific implementation of this async function. It starts out by calling promise-returning functions for all replacements, which usually means starting separate and parallel asynchronous tasks for each one:
const replacements = await Promise.all(
Array.from(string.matchAll(regexp),
match => replacerFunction(...match)));
So if you don’t want these tasks to run in parallel, wait until the previous one has finished before calling the next:
const replacements = [];
for (const match of string.matchAll(regexp)) {
replacements.push(await replacerFunction(...match));
}