javascript : Async/await in .replace

13,009

Solution 1

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())
        });
    });
}

Solution 2

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("");
    });
}

Solution 3

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("");
Share:
13,009
ritz078
Author by

ritz078

Updated on June 16, 2022

Comments

  • ritz078
    ritz078 almost 2 years

    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.

    • Madara's Ghost
      Madara's Ghost over 8 years
      The return value from an async function is always a Promise object that resolves with the returned output (or rejects with the thrown error).
    • Felix Kling
      Felix Kling over 8 years
      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.
  • ritz078
    ritz078 over 8 years
    I have updated the question. I need to pass the matched element to the function so this way that can't be done.
  • spender
    spender over 8 years
    @ritz078 I thought you might have missed that out. Perhaps my edit is more useful?
  • Jack G
    Jack G over 5 years
    This only works when using replace to iterate over matches. This does not work for replacement.
  • Overcl9ck
    Overcl9ck over 5 years
    It does though. It iterates, and replaces.
  • Livewire
    Livewire about 5 years
    I really like this solution, nice and simple!