Retry a Promise resolution N times, with a delay between the attempts
Call stack / Job queue / Event queue

Calling an async function does not use the call stack in the way you may be used to.

Rather the async call remains on the call stack only until an await is encountered. Then the call is removed from the call stack and the await (a micro task (I think it is called)) is placed in the job queue (new to ES6). Micro tasks are very similar to the events but have higher priority (micro tasks will be executed before events)

That means that async functions can not overflow the call stack if you call it recursively as long as there is an await before the recursion. 

Awaiting the recursion call will not work as the code to the right of await is executed before the current context is removed from the call stack.

The following JS example shows a recursive count down from 1billion (it will take some time but will not throw a call stack overflow)

Eg

async function recursive(c = 1e9) { // 1 billion
    if (c &gt; 0) { 
        await Promise.resolve(); // removes call recursive from call stack
                                 // adds this micro task to the job queue
                                 // Execution is idle and thus the micro task
                                 // is removed from the queue and next line is
                                 // executed
        return recursive(c - 1);
    }
    return c;
}
recursive().then(console.log); // will in time display 0


Recursion

There is no danger in using a recursive and less complex style for your solution. The example below outlines the principle in JS (I am too lazy to type typescript)

const MAX_TRYS = 10; TRY_TIMEOUT = 500;
function toTry() {
    return new Promise((ok, fail) =&gt; { 
        setTimeout(() =&gt; Math.random() &lt; 0.05 ? ok("OK!") : fail("Error"), TRY_TIMEOUT);
    });
}
async function tryNTimes(toTry, count = MAX_TRYS) {
    if (count &gt; 0) {
        const result = await toTry().catch(e =&gt; e);
        if (result === "Error") { return await tryNTimes(toTry, count - 1) }             
        return result
    }
    return `Tried ${MAX_TRYS} times and failed`;
}

tryNTimes(toTry).then(console.log);


Promise resolve/reject are micro tasks

I will point out that even if you used just plain old promises rather than a async function the result is the same. A promise adds a micro task to the job queue and will not resolve or reject until the current call stack is empty.

It is therefor safe to recursively call the calling function from the resolved/rejected promise.