37

I am using the async/await function the following way

async function(){
  let output = await string.replace(regex, async (match)=>{
    let data = await someFunction(match)
    console.log(data); //gives correct data
    return data
  })
  return output;
}

But the returned data is an promise object. Just confused about the way it should be implemented in such functions with callback.

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
ritz078
  • 2,193
  • 3
  • 22
  • 24
  • The return value from an `async` function is always a Promise object that resolves with the returned `output` (or rejects with the thrown error). – Madara's Ghost Nov 10 '15 at 13:25
  • Are you wondering why `output` is a promise? It's unclear to me what your issue is. Note that if `string.replace` is literally `String.prototype.replace`, then that won't work. `.replace` expects the callback to be a normal function, not an async function. – Felix Kling Nov 10 '15 at 16:17

7 Answers7

49

An easy function to use and understand for some async replace :

async function replaceAsync(str, regex, asyncFn) {
    const promises = [];
    str.replace(regex, (match, ...args) => {
        const promise = asyncFn(match, ...args);
        promises.push(promise);
    });
    const data = await Promise.all(promises);
    return str.replace(regex, () => data.shift());
}

It does the replace function twice so watch out if you do something heavy to process. For most usages though, it's pretty handy.

Use it like this:

replaceAsync(myString, /someregex/g, myAsyncFn)
    .then(replacedString => console.log(replacedString))

Or this:

const replacedString = await replaceAsync(myString, /someregex/g, myAsyncFn);

Don't forget that your myAsyncFn has to return a promise.

An example of asyncFunction :

async function myAsyncFn(match) {
    // match is an url for example.
    const fetchedJson = await fetch(match).then(r => r.json());
    return fetchedJson['date'];
}

function myAsyncFn(match) {
    // match is a file
    return new Promise((resolve, reject) => {
        fs.readFile(match, (err, data) => {
            if (err) return reject(err);
            resolve(data.toString())
        });
    });
}
Overcl9ck
  • 828
  • 7
  • 11
8

The native replace method does not deal with asynchronous callbacks, you cannot use it with a replacer that returns a promise.

We can however write our own replace function that deals with promises:

async function(){
  return string.replace(regex, async (match)=>{
    let data = await someFunction(match)
    console.log(data); //gives correct data
    return data;
  })
}

function replaceAsync(str, re, callback) {
    // http://es5.github.io/#x15.5.4.11
    str = String(str);
    var parts = [],
        i = 0;
    if (Object.prototype.toString.call(re) == "[object RegExp]") {
        if (re.global)
            re.lastIndex = i;
        var m;
        while (m = re.exec(str)) {
            var args = m.concat([m.index, m.input]);
            parts.push(str.slice(i, m.index), callback.apply(null, args));
            i = re.lastIndex;
            if (!re.global)
                break; // for non-global regexes only take the first match
            if (m[0].length == 0)
                re.lastIndex++;
        }
    } else {
        re = String(re);
        i = str.indexOf(re);
        parts.push(str.slice(0, i), callback.apply(null, [re, i, str]));
        i += re.length;
    }
    parts.push(str.slice(i));
    return Promise.all(parts).then(function(strings) {
        return strings.join("");
    });
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
6

So, there's no overload of replace that takes a promise. So simply restate your code:

async function(){
  let data = await someFunction();
  let output = string.replace(regex, data)
  return output;
}

of course, if you need to use the match value to pass to the asynchronous function, things get a bit more complicated:

var sourceString = "sheepfoohelloworldgoocat";
var rx = /.o+/g;

var matches = [];
var mtch;
rx.lastIndex = 0; //play it safe... this regex might have state if it's reused
while((mtch = rx.exec(sourceString)) != null)
{
    //gather all of the matches up-front
    matches.push(mtch);
}
//now apply async function someFunction to each match
var promises = matches.map(m => someFunction(m));
//so we have an array of promises to wait for...
//you might prefer a loop with await in it so that
//you don't hit up your async resource with all
//these values in one big thrash...
var values = await Promise.all(promises);
//split the source string by the regex,
//so we have an array of the parts that weren't matched
var parts = sourceString.split(rx);
//now let's weave all the parts back together...
var outputArray = [];
outputArray.push(parts[0]);
values.forEach((v, i) => {
    outputArray.push(v);
    outputArray.push(parts[i + 1]);
});
//then join them back to a string... voila!
var result = outputArray.join("");
spender
  • 117,338
  • 33
  • 229
  • 351
  • I have updated the question. I need to pass the matched element to the function so this way that can't be done. – ritz078 Nov 10 '15 at 13:31
  • @ritz078 I thought you might have missed that out. Perhaps my edit is more useful? – spender Nov 10 '15 at 13:59
5

Here’s an improved and more modern version of Overcl9ck’s answer:

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++]);
}

This requires a newer browser baseline due to String.prototype.matchAll, which landed across the board in 2019 (except Edge which got it in early 2020 with the Chromium-based Edge). But it’s at least as simple while also being more efficient, only matching the first time through, rather than creating a useless string, and not mutating the array of replacements in an expensive way.

Chris Morgan
  • 86,207
  • 24
  • 208
  • 215
  • You can drop `let i = 0` and use `() => replacements.shift()` to shave a line. – lapo Jan 03 '23 at 23:26
  • 2
    @lapo: I ditched the `shift` usage deliberately, because it’s very bad for performance, turning something linear and read-only quadratic with writing for no good reason. (I mentioned this in the answer.) – Chris Morgan Jan 04 '23 at 14:24
  • That makes perfect sense, thanks. I never thought `.shift()` was expensive but you're most probably right; now I have to go into that rabbit hole though… indeed much slower: https://jsbench.me/m4lclyqpsp/1 – lapo Jan 07 '23 at 13:10
3

This is Overcl9ck's solution implemented in TypeScript:

const replaceAsync = async (str: string, regex: RegExp, asyncFn: (match: any, ...args: any) => Promise<any>) => {
    const promises: Promise<any>[] = []
    str.replace(regex, (match, ...args) => {
        promises.push(asyncFn(match, ...args))
        return match
    })
    const data = await Promise.all(promises)
    return str.replace(regex, () => data.shift())
}
Enzo Ferey
  • 163
  • 2
  • 6
Ricardo Yubal
  • 374
  • 3
  • 8
1

And yet another solution, this time in TypeScript. Similar to Maxime's solution, it avoids the "semantically unusual" initial replace()-call in many of the other solutions by using match() instead.

async function replaceAsync(str: string, regex: RegExp, asyncFn: (match: string) => Promise<string>): Promise<string> {
  const promises = (str.match(regex) ?? []).map((match: string) => asyncFn(match));
  const data = await Promise.all(promises);
  return str.replace(regex, () => data.shift()!);
}
Rene Hamburger
  • 2,003
  • 16
  • 17
0

Here is a pretty alternative method using a recursive function:

async function replaceAsync(str, regex, asyncFn) {
    const matches = str.match(regex);
    if (matches) {
        const replacement = await asyncFn(...matches);
        str = str.replace(matches[0], replacement);
        str = await replaceAsync(str, regex, asyncFn);
    }
    return str;
}
maxime schoeni
  • 2,666
  • 2
  • 18
  • 19
  • 1. str.replace gets a string with no position, it might replace same string in other places that did not match the regex. 2. The recursion assumes the replacement will no longer match the regex, so next time around it will find the _next_ match. If it does match, it risks looping forever! 3. even if not forever, one place undergoing several replacements can give unintended results. 4. if replacement doesn't match/overlap, it still might affect the location of later matches — via `^`, `\b` boundaries or generic lookbehind (ES9). 5. O(n²) performance see "Schlemiel the Painter" – Beni Cherniavsky-Paskin Feb 14 '23 at 11:51