Recursive Promise in 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'));

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

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>