# XMLHttpRequest with cancel, timeout and retry option

I've made the following code-snippet, trying to achieve a replacement for Fetch Which have the abortion, timeout and retry options.

It worked for me, but i'm not sure if it is a good idea to do so or not. Any better ideas?

const customXmlHttpRequest = (url, opts={}, cancel={}, timeoutMS = 30000) => {

return new Promise((resolve, reject)=>{

var xhr = new XMLHttpRequest();
xhr.open(opts.method || 'get', url);
xhr.timeout = timeoutMS;
for (var k in opts.headers || {}){
}
cancel.abort = () => {
xhr.abort();
reject("Cancelled")
};
xhr.onerror = reject;
xhr.ontimeout = () => {
reject("Timeout")
}
xhr.send(opts.body);

});

}

const FetchWithRetry = (url, opts={}, retry=3, retryDelay=1000, cancel={}, timeoutMS = 30000) => {
return new Promise((resolve, reject)=>{
var counter = 0;

const FetchWithTimeout = ()=>{
if(counter < retry){
counter++
customXmlHttpRequest(url, opts, cancel={}, timeoutMS = 30000)
.then((res)=>{
resolve(res)
})
.catch((e)=>{
if(e === "Cancelled"){
reject(e)
} else if(e === "Timeout"){
///retry
setTimeout(()=>{
FetchWithTimeout()
}, retryDelay)
} else if(String(e).includes("Network request failed")){
/// retry
setTimeout(()=>{
FetchWithTimeout()
}, retryDelay)
} else {
reject(e)
}
})
}
}
FetchWithTimeout()
})
}

• Just an observation, since body can be sent only with 'put', you may want to add a ternary to the fallback in xhr.open. Apr 21 '17 at 13:15
• @wOxxOm I'll look into that. thanks. any other ideas/changes? Apr 21 '17 at 13:18

First of all, there's already a fetch function that's already available in most modern browsers. If you care about IE, there's also a polyfill for it. With that out of the way, we can ignore the first part of your code and focus on the second part of the code.

While it's perfectly fine JS, having very long argument lists is horrible. As a developer, you have to evaluate which arguments take priority and come first. As a consumer, you have to remember which ones come first. I suggest turning your options into an object. You can use Object.assign to merge a defaults object.

With regards to your control flow, you can simplify the retry routine by simply calling fetchWithRetry recursively, passing it the retries remaining. Since this operates on just promises and promises cannot be cancelled, this version's abort bails out when abort is called during the delay or after the request, not immediately.

const fetchWithRetry = (userOptions) => {
let abort = false;
const options = {
url: '',
options: {},
cancel: {},
retries: 3,
retryDelay: 1000,
...userOptions
};

// Add an abort to the cancel object.
cancel.abort = () => { abort = true };

// Abort or proceed?
return abort ? Promise.reject('aborted') : fetch(options.url).then(response => {

// Reject because of abort
return abort ? Promise.reject('aborted')

// Response is good
: response.ok ? Promise.resolve(response.text())

// Retries exceeded
: !options.retries ? Promise.reject('retries exceeded')

// Retry with one less retry
: new Promise((resolve, reject){
setTimeout(() => {

// We use the returned promise's resolve and reject as
// callback so that the nested call propagates backwards.
fetchWithRetry({ ...options, retries: retries - 1 }).then(resolve, reject);
}, options.retryDelay);

});
});
}