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!
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!
Share Improve this question asked Aug 1, 2017 at 17:49 mikewagzmikewagz 3791 gold badge6 silver badges15 bronze badges 4 |5 Answers
Reset to default 5Here'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)
}
}
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)
)
}
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'));
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)
})
}
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.sendFile(params, --retries)
Try this instead:this.sendFiles(params, --retries).resolve(resolve, reject)
– Antony Commented Aug 1, 2017 at 17:52