Promise retries until success/failure with Typescript

Viewed 9689

My mobile app uploads several files to the server in succession, often from remote areas with questionable connection strength. For this reason, I want to make a few attempts to send the file. I also want to move on and attempt the next one in the event of a failure, with all error messages displayed at the end of the export (ie "10 files uploaded, 3 failed...")

However, I'm having trouble figuring out the recursive retry pattern with promises. Here's what I have so far:

sendFile(params, retries = 3){
    console.log("SENDING FILE: ", retries, "attempts remaining", params)

    return new Promise((resolve, reject)=>{
      if(retries > 0){
        this._sendFile(params)
        .then(()=>{
          // Upload Success
          console.log("Upload Success!")
          resolve()
        })
        .catch((err)=>{
          console.log("Upload Fail", err)
          // Retry
          this.sendFile(params, --retries)
        })
      }else{
        console.log("Failed 3 times!!!")
        //Record error
        this.exportStatus.errors.push({
          message:"A file failed to upload after 3 attempts.",
          params: params
        })

        //Resolve and move on to next export
        resolve()

      }
    })

  }

  _sendFile(params){

      // Mobile - File Transfer

        let options = {
          fileKey: "file_transfer",
          fileName: params.fileName,
          httpMethod: "PUT",
          chunkedMode: false,
          headers: {
            "x-user-email":this.settings.user.email,
            "x-user-token":this.settings.user.authentication_token,
          }
        }

        let fileTransfer = this.transfer.create()
        let url = encodeURI(this.settings.api_endpoint + params.url)

        return fileTransfer.upload(params.file, url, options, true)


  }

When I raise an exception on the server, I'll see the "Failed 3 times!!!" error message, but the calling promise does not resolve for the rest of the export to move on. I believe this is because I'm created nested promises (ie creating a new promise with each retry). How can I have the original promise resolve after 3 retries?

Thanks!

5 Answers

A follow up on what Patrick Roberts was posted, an example of how to implement it in typescript:

type promiseExecutor<T> = (resolve: (value?: T | PromiseLike<T>) => void, reject: (reason?: any) => void) => void;
class RetryablePromise<T> extends Promise<T> {
  static retry<T>(retries: number, executor: promiseExecutor<T>): Promise<T> {
    return new RetryablePromise(executor).catch(error =>
      retries > 0 ? RetryablePromise.retry(retries - 1, executor) : RetryablePromise.reject(error)
    );
  }
}

and usage is as follows:

RetryablePromise.retry(4, (resolve, reject) => console.log('run'));

Here's a simple version for completeness sake:

export async function promiseRetry<T>(fn: () => Promise<T>, retries = 5, err?: any): Promise<T> {
  await new Promise(resolve => setTimeout(resolve, (5 - retries) * 1000));

  return !retries ? Promise.reject(err) : fn().catch(error => promiseRetry(fn, (retries - 1), error));
}

Has built-in retry logic, which can be commented out if not needed.

Usage:

const myVal = await promiseRetry(() => myPromiseFn())

This is a minor improvement on the answer provided by DarkNeuron, that does not create a timeout before first attempt, and uses a constant configurable delay between retries.

const MAX_RETRIES_DEFAULT = 5

export async function promiseRetry<T>(
  fn: () => Promise<T>,
  retries = MAX_RETRIES_DEFAULT,
  retryIntervalMillis: number,
  previousError?: Error
): Promise<T> {
  return !retries
    ? Promise.reject(previousError)
    : fn().catch(async (error) => {
        await new Promise((resolve) => setTimeout(resolve, retryIntervalMillis))
        return promiseRetry(fn, retries - 1, retryIntervalMillis, error)
      })
}
Related