Recursive Promise in javascript

JavascriptRecursionPromise

Javascript Problem Overview


I'm writing a Javascript Promise that finds the final redirect URL of a link.

What I'm doing is making a HEAD request in a Promise using an XMLHttpRequest. Then, on load, check the HTTP Status for something in the 300 range, or if it has a responseURL attached to the object and that url is different than the it was one handed.

If neither of these are true, I resolve(url). Otherwise, I recursively call getRedirectUrl() on the response URL, and resolve().

Here's my code:

function getRedirectUrl(url, maxRedirects) {
    maxRedirects = maxRedirects || 0;
    if (maxRedirects > 10) {
        throw new Error("Redirected too many times.");
    }

    var xhr = new XMLHttpRequest();
    var p = new Promise(function (resolve) {
        xhr.onload = function () {
            var redirectsTo;
            if (this.status < 400 && this.status >= 300) {
                redirectsTo = this.getResponseHeader("Location");
            } else if (this.responseURL && this.responseURL != url) {
                redirectsTo = this.responseURL;
            }

            if (redirectsTo) {
                // check that redirect address doesn't redirect again
                // **problem line**
                p.then(function () { self.getRedirectUrl(redirectsTo, maxRedirects + 1); });
                resolve();
            } else {
                resolve(url);
            }
        }

        xhr.open('HEAD', url, true);
        xhr.send();
    });

    return p;
}

Then to use this function I do something like:

getRedirectUrl(myUrl).then(function (url) { ... });

The issue is that resolve(); in getRedirectUrl will call the then() from the calling function before it calls the getRedirectUrl recursive call, and at that point, the URL is undefined.

I tried, rather than p.then(...getRedirectUrl...) doing return self.getRedirectUrl(...) but this will never resolve.

My guess is that the pattern I'm using (that I basically came up with on the fly) isn't right, altogether.

Javascript Solutions


Solution 1 - Javascript

The problem is that the promise you return from getRedirectUrl() needs to include the entire chain of logic to get to the URL. You're just returning a promise for the very first request. The .then() you're using in the midst of your function isn't doing anything.

To fix this:

Create a promise that resolves to redirectUrl for a redirect, or null otherwise:

function getRedirectsTo(xhr) {
    if (xhr.status < 400 && xhr.status >= 300) {
        return xhr.getResponseHeader("Location");
    }
    if (xhr.responseURL && xhr.responseURL != url) {
        return xhr.responseURL;
    }

    return null;
}

var p = new Promise(function (resolve) {
    var xhr = new XMLHttpRequest();

    xhr.onload = function () {
        resolve(getRedirectsTo(xhr));
    };

    xhr.open('HEAD', url, true);
    xhr.send();
});

Use .then() on that to return the recursive call, or not, as needed:

return p.then(function (redirectsTo) {
    return redirectsTo
        ? getRedirectUrl(redirectsTo, redirectCount+ 1)
        : url;
});

Full solution:

function getRedirectsTo(xhr) {
    if (xhr.status < 400 && xhr.status >= 300) {
        return xhr.getResponseHeader("Location");
    }
    if (xhr.responseURL && xhr.responseURL != url) {
        return xhr.responseURL;
    }

    return null;
}

function getRedirectUrl(url, redirectCount) {
    redirectCount = redirectCount || 0;

    if (redirectCount > 10) {
        throw new Error("Redirected too many times.");
    }

    return new Promise(function (resolve) {
        var xhr = new XMLHttpRequest();

        xhr.onload = function () {
            resolve(getRedirectsTo(xhr));
        };

        xhr.open('HEAD', url, true);
        xhr.send();
    })
    .then(function (redirectsTo) {
        return redirectsTo
            ? getRedirectUrl(redirectsTo, redirectCount + 1)
            : url;
    });
}

Solution 2 - Javascript

Here's the simplified solution:

const recursiveCall = (index) => {
    return new Promise((resolve) => {
        console.log(index);
        if (index < 3) {
            return resolve(recursiveCall(++index))
        } else {
            return resolve()
        }
    })
}

recursiveCall(0).then(() => console.log('done'));

Solution 3 - Javascript

The following has two functions:

  • _getRedirectUrl - which is a setTimeout object simulation for looking up a single step lookup of a redirected URL (this is equivalent to a single instance of your XMLHttpRequest HEAD request)
  • getRedirectUrl - which is recursive calls Promises to lookup the redirect URL

The secret sauce is the sub Promise whose's successful completion will trigger a call to resolve() from the parent promise.

function _getRedirectUrl( url ) {
    return new Promise( function (resolve) {
        const redirectUrl = {
            "https://mary"   : "https://had",
            "https://had"    : "https://a",
            "https://a"      : "https://little",
            "https://little" : "https://lamb",
        }[ url ];
        setTimeout( resolve, 500, redirectUrl || url );
    } );
}

function getRedirectUrl( url ) {
    return new Promise( function (resolve) {
        console.log("* url: ", url );
        _getRedirectUrl( url ).then( function (redirectUrl) {
            // console.log( "* redirectUrl: ", redirectUrl );
            if ( url === redirectUrl ) {
                resolve( url );
                return;
            }
            getRedirectUrl( redirectUrl ).then( resolve );
        } );
    } );
}

function run() {
    let inputUrl = $( "#inputUrl" ).val();
    console.log( "inputUrl: ", inputUrl );
    $( "#inputUrl" ).prop( "disabled", true );
    $( "#runButton" ).prop( "disabled", true );
    $( "#outputLabel" ).text( "" );
    
    getRedirectUrl( inputUrl )
    .then( function ( data ) {
        console.log( "output: ", data);
        $( "#inputUrl" ).prop( "disabled", false );
        $( "#runButton" ).prop( "disabled", false );
        $( "#outputLabel").text( data );
    } );

}

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

Input:

<select id="inputUrl">
    <option value="https://mary">https://mary</option>
    <option value="https://had">https://had</option>
    <option value="https://a">https://a</option>
    <option value="https://little">https://little</option>
    <option value="https://lamb">https://lamb</option>
</select>

Output:

<label id="outputLabel"></label>

<button id="runButton" onclick="run()">Run</button>

As another illustration of recursive Promises, I used it to solve a maze. The Solve() function is invoked recursively to advance one step in a solution to a maze, else it backtracks when it encounters a dead end. The setTimeout function is used to set the animation of the solution to 100ms per frame (i.e. 10hz frame rate).

const MazeWidth = 9
const MazeHeight = 9

let Maze = [
    "# #######",
    "#   #   #",
    "# ### # #",
    "# #   # #",
    "# # # ###",
    "#   # # #",
    "# ### # #",
    "#   #   #",
    "####### #"
].map(line => line.split(''));

const Wall = '#'
const Free = ' '
const SomeDude = '*'

const StartingPoint = [1, 0]
const EndingPoint = [7, 8]

function PrintDaMaze()
{
    //Maze.forEach(line => console.log(line.join('')))
    let txt = Maze.reduce((p, c) => p += c.join('') + '\n', '')
    let html = txt.replace(/[*]/g, c => '<font color=red>*</font>')
    $('#mazeOutput').html(html)
}

function Solve(X, Y) {

    return new Promise( function (resolve) {
    
        if ( X < 0 || X >= MazeWidth || Y < 0 || Y >= MazeHeight ) {
            resolve( false );
            return;
        }
        
        if ( Maze[Y][X] !== Free ) {
            resolve( false );
            return;
        }

        setTimeout( function () {
        
            // Make the move (if it's wrong, we will backtrack later)
            Maze[Y][X] = SomeDude;
            PrintDaMaze()

            // Check if we have reached our goal.
            if (X == EndingPoint[0] && Y == EndingPoint[1]) {
                resolve(true);
                return;
            }

            // Recursively search for our goal.
            Solve(X - 1, Y)
            .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X + 1, Y);
            } )
            .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X, Y - 1);
             } )
             .then( function (solved) {
                if (solved) return Promise.resolve(solved);
                return Solve(X, Y + 1);
             } )
             .then( function (solved) {
                 if (solved) {
                     resolve(true);
                     return;
                 }

                 // Backtrack
                 setTimeout( function () {
                     Maze[Y][X] = Free;
                     PrintDaMaze()
                     resolve(false);
                 }, 100);
                 
             } );

        }, 100 );
    } );
}

Solve(StartingPoint[0], StartingPoint[1])
.then( function (solved) {
    if (solved) {
        console.log("Solved!")
        PrintDaMaze()
    }
    else
    {
        console.log("Cannot solve. :-(")
    }
} );

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

<pre id="mazeOutput">
</pre>

Solution 4 - Javascript

Please check below example it will return factorial of a given number as we did in many programming languages.

I have implemented below example using JavaScript promises.

let code = (function(){
	let getFactorial = n =>{
		return new Promise((resolve,reject)=>{
			if(n<=1){
				resolve(1);
			}
			resolve(
				getFactorial(n-1).then(fact => {
					return fact * n;
				})
			)
		});
	}
	return {
		factorial: function(number){
			getFactorial(number).then(
				response => console.log(response)
			)
		}
	}
})();
code.factorial(5);
code.factorial(6);
code.factorial(7);

Solution 5 - Javascript

If you're in an environment that supports async/await (virtually all modern environments do), you can write an async function that looks a bit more like a recursive function pattern we all know and love. It's not possible to completely avoid a Promise due to the nature of XMLHttpRequest only retrieving a value via the load event (rather than exposing a Promise itself), but the recursive nature of the function that makes the call should look familiar.

Having four more years of JavaScript experience than I had when I originally wrote this question, I cleaned up the code a bit, but it works essentially the same way.

// creates a simple Promise that resolves the xhr once it has finished loading
function createXHRPromise(url) {
    return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();

        // addEventListener('load', ...) is basically the same as setting
        // xhr.onload, but is better practice
        xhr.addEventListener('load', () => resolve(xhr));

        // throw in some error handling so that the calling function 
        // won't hang
        xhr.addEventListener('error', reject);
        xhr.addEventListener('abort', reject);

        xhr.open('HEAD', url, true);
        xhr.send();
    });
}

async function getRedirectUrl(url, maxRetries = 10) {
    if (maxRetries <= 0) {
        throw new Error('Redirected too many times');
    }

    const xhr = await createXHRPromise(url);
    if (xhr.status >= 300 && xhr.status < 400) {
        return getRedirectUrl(xhr.getResponseHeader("Location"), maxRetries - 1);
    } else if (xhr.responseURL && xhr.responseURL !== url) {
        return getRedirectUrl(xhr.responseURL, maxRetries - 1);
    }

    return url;
}
A brief explanation of async/await
  • an async function is syntactic sugar for a Promise
  • await is syntactic sugar for Promise.then()
  • return within an async function is syntactic sugar for resolve()
  • throw within an async function is syntactic sugar for reject()

If an async function returns either another async function call or a Promise, the function/promise will resolve before the original call resolves, exactly the same way that resolving a Promise would in the Promise pattern.

So, you can call getRedirectUrl(someUrl).then(...).catch(...) exactly the same way the original question would have.

It should probably be noted that using an XHR to resolve a redirected URL will fail for any URL that doesn't include the proper CORS header.


As an added bonus, async/await makes an iterative approach trivial.

async function getRedirectUrl(url, maxRetries = 10) {
    for (let i = 0; i < maxRetries; i++) {
        const xhr = await createXHRPromise(url);
        if (xhr.status >= 300 && xhr.status < 400) {
            url = xhr.getResponseHeader("Location");
        } else if (xhr.responseURL && xhr.responseURL !== url) {
            url = xhr.responseURL;
        } else {
            return url;
        }
    }

    throw new Error('Redirected too many times');
}

Another note: modern browsers have a fetch() function that does essentially what createXHRPromise() does, but is more versatile. It's not supported in node, but there is an npm package called node-fetch.

Solution 6 - Javascript

Please check below example for Recursive Promise in javascript/typescript, It will not resolve the promise until number incremented to greater then 13.

below code is suitable for typescript and modify it slightly for javascript.

async iterate(number: number): Promise<any> {
        return new Promise((resolve, reject) => {
            let number = 0;
            if (number > 13) {
                // recursive terminate condition
                resolve(number);
                return;
            } else {
                number = number + 1;
                // recursive call
                this.iterate(number).then(resolve);
            }

        });
    }




this.iterate().then((resolvedData: any) => {
           // wait until number is not greater than 13
           console.log(resolvedData);
    });

Solution 7 - Javascript

If you have a nested array structure with asynchronous calls, this solution (built on previous answers) might help. The example runs a setTimeout() for each value it finds in a (possibly) nested array and resolves when it's done with all of them:

const recursiveCall = (obj) => {
    return new Promise((resolve) => {
        if(obj instanceof Array){
            let cnt = obj.length;
            obj.forEach(el => {
                recursiveCall(el)
                .then(() => {
                    if(!--cnt)return resolve();
                })
                
            });
        } else {
            setTimeout(() => {
                console.log(obj);
                return resolve();
            }, obj);
            
        }
    })
}

recursiveCall([100,50,[10,[200, 300],30],1]).then(() => console.log('done'));

>1
>10
>30
>50
>100
>200
>300
>done

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questiondx_over_dtView Question on Stackoverflow
Solution 1 - JavascriptJLRisheView Answer on Stackoverflow
Solution 2 - JavascriptcuddlemeisterView Answer on Stackoverflow
Solution 3 - JavascriptStephen QuanView Answer on Stackoverflow
Solution 4 - JavascriptMayur ShedageView Answer on Stackoverflow
Solution 5 - Javascriptdx_over_dtView Answer on Stackoverflow
Solution 6 - JavascriptRavindra VairagiView Answer on Stackoverflow
Solution 7 - JavascriptHans LindetorpView Answer on Stackoverflow