Recursive Promise in javascript
JavascriptRecursionPromiseJavascript 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;
}
async
/await
A brief explanation of - an
async function
is syntactic sugar for aPromise
await
is syntactic sugar forPromise.then()
return
within anasync function
is syntactic sugar forresolve()
throw
within anasync function
is syntactic sugar forreject()
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