mikewagz
mikewagz

Reputation: 379

Promise retries until success/failure with Typescript

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!

Upvotes: 5

Views: 16101

Answers (5)

Gudlaugur Egilsson
Gudlaugur Egilsson

Reputation: 2460

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

Upvotes: 2

Patrick Roberts
Patrick Roberts

Reputation: 51756

You could implement a wrapper for Promise() that automatically chains retries for you, allowing you to refactor your code with whatever logic you need and not worry about handling retry logic simultaneously. Your usage could look something like this:

sendFile(params, retries = 3) {
  return Promise.retry(retries, (resolve, reject) => {
    this._sendFile(params).then(resolve, reject)
  })
}

Below is how you could implement Promise.retry():

Object.defineProperty(Promise, 'retry', {
  configurable: true,
  writable: true,
  value: function retry (retries, executor) {
    console.log(`${retries} retries left!`)

    if (typeof retries !== 'number') {
      throw new TypeError('retries is not a number')
    }

    return new Promise(executor).catch(error => retries > 0
      ? Promise.retry(retries - 1, executor)
      : Promise.reject(error)
    )
  }
})

Promise.retry(100, (resolve, reject) => {
  // your sendFile core logic with proper
  // calls to resolve and reject goes here
  const rand = Math.random()

  console.log(rand)

  if (rand < 0.1) resolve(rand)
  else reject(rand)
}).then(
  value => console.log(`resolved: ${value}`),
  error => console.log(`rejected: ${error}`)
)

If you're uncomfortable extending a native object (this would be the correct way to do so, since it is a configurable, non-enumerable and writable property), you can just implement it as a static function:

function retry (retries, executor) {
  console.log(`${retries} retries left!`)

  if (typeof retries !== 'number') {
    throw new TypeError('retries is not a number')
  }

  return new Promise(executor).catch(error => retries > 0
    ? retry(retries - 1, executor)
    : Promise.reject(error)
  )
}

Upvotes: 3

DarkNeuron
DarkNeuron

Reputation: 8682

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())

Upvotes: 1

Max
Max

Reputation: 1175

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

Upvotes: 2

mikewagz
mikewagz

Reputation: 379

Here's what ended up working:

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

    if(retries > 0){
      return this._sendFile(params)
      .then(()=>{
        // Upload Success
        console.log("Upload Success!")
        return Promise.resolve(true)
      })
      .catch((err)=>{
        console.log("Upload Fail", err)

        this.exportStatus.retries++

        return this.sendFile(params, --retries) // <-- The important part
      })
    }else{
      console.log("Failed 3 times!!!")

      this.exportStatus.errors.push({
        message:"A file failed to upload after 3 attempts.",
        params: params
      })

      return Promise.resolve(false)

    }

  }

Upvotes: 5

Related Questions