How do I promisify native XHR?
I'm assuming you know how to make a native XHR request (you can brush up here and here)
Since any browser that supports native promises will also support xhr.onload
, we can skip all the onReadyStateChange
tomfoolery. Let's take a step back and start with a basic XHR request function using callbacks:
function makeRequest (method, url, done) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
done(null, xhr.response);
};
xhr.onerror = function () {
done(xhr.response);
};
xhr.send();
}
// And we'd call it as such:
makeRequest('GET', 'http://example.com', function (err, datums) {
if (err) { throw err; }
console.log(datums);
});
Hurrah! This doesn't involve anything terribly complicated (like custom headers or POST data) but is enough to get us moving forwards.
The promise constructor
We can construct a promise like so:
new Promise(function (resolve, reject) {
// Do some Async stuff
// call resolve if it succeeded
// reject if it failed
});
The promise constructor takes a function that will be passed two arguments (let's call them resolve
and reject
). You can think of these as callbacks, one for success and one for failure. Examples are awesome, let's update makeRequest
with this constructor:
function makeRequest (method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
xhr.send();
});
}
// Example:
makeRequest('GET', 'http://example.com')
.then(function (datums) {
console.log(datums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
Now we can tap into the power of promises, chaining multiple XHR calls (and the .catch
will trigger for an error on either call):
makeRequest('GET', 'http://example.com')
.then(function (datums) {
return makeRequest('GET', datums.url);
})
.then(function (moreDatums) {
console.log(moreDatums);
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
We can improve this still further, adding both POST/PUT params and custom headers. Let's use an options object instead of multiple arguments, with the signature:
{
method: String,
url: String,
params: String | Object,
headers: Object
}
makeRequest
now looks something like this:
function makeRequest (opts) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(opts.method, opts.url);
xhr.onload = function () {
if (xhr.status >= 200 && xhr.status < 300) {
resolve(xhr.response);
} else {
reject({
status: xhr.status,
statusText: xhr.statusText
});
}
};
xhr.onerror = function () {
reject({
status: xhr.status,
statusText: xhr.statusText
});
};
if (opts.headers) {
Object.keys(opts.headers).forEach(function (key) {
xhr.setRequestHeader(key, opts.headers[key]);
});
}
var params = opts.params;
// We'll need to stringify if we've been given an object
// If we have a string, this is skipped.
if (params && typeof params === 'object') {
params = Object.keys(params).map(function (key) {
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
}).join('&');
}
xhr.send(params);
});
}
// Headers and params are optional
makeRequest({
method: 'GET',
url: 'http://example.com'
})
.then(function (datums) {
return makeRequest({
method: 'POST',
url: datums.url,
params: {
score: 9001
},
headers: {
'X-Subliminal-Message': 'Upvote-this-answer'
}
});
})
.catch(function (err) {
console.error('Augh, there was an error!', err.statusText);
});
A more comprehensive approach can be found at MDN.
Alternatively, you could use the fetch API (polyfill).
xhr promise returns error
I guess the problem is on promise resolving. Try to combine received data into single object and pass it to resolve()
setInterval(function() {
let requestPromise = new Promise((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.open('POST', "location?act=update&id=<?php echo $id; ?>", true);
xhr.send();
xhr.onreadystatechange = function(){
if(this.readyState==4 && this.status == 200 && this.responseText){
try{
var data = JSON.parse(this.responseText);
if(data.command=="list"){
resolve({
answerLat: parseFloat(data.lat),
answerLng: parseFloat(data.lng),
answerAccuracy: data.accuracy,
answerTime: data.time
});
}
}catch(e){
reject(e)
}
}
}
});
requestPromise.then(googleMapsFunction); // googleMapsFunction will be called once the request is completed and will get answerLat as the argument
}, 20000);
const googleMapsFunction = (params) => {
const {answerLat, answerLng, answerAccuracy, answerTime} = params
// ... answerLat, answerLng, answerAccuracy, answerTime
}
How to retry an xhr request which returns a promise recursively for atleast n times on status 0
Here's how I'd approach it (see ***
comments):
var makeRequest = function(method, urlToBeCalled, payload) {
var deferred = $q.defer();
var retries = 4; // *** Counter
run(); // *** Call the worker
return deferred.promise;
// *** Move the actual work to its own function
function run() {
var xhr = new XMLHttpRequest();
xhr.open(method, encodeURI(urlToBeCalled), true);
setHttpRequestHeaders(xhr);
xhr.onload = function() {
if (xhr.status === 200 && xhr.readyState === 4 && xhr.getResponseHeader('content-type') !== 'text/html') {
try {
response = JSON.parse(xhr.response);
deferred.resolve(response);
} catch (e) {
deferred.reject(e);
}
} else if (xhr.status === 0) {
// retry
if (retries--) { // *** Recurse if we still have retries
run();
} else {
// *** Out of retries
deferred.reject(e);
}
} else {
// *** See note below, probably remove this
try {
response = JSON.parse(xhr.response);
deferred.reject(response);
} catch (e) {
deferred.reject(xhr.response);
}
}
};
xhr.onerror = function() {
deferred.reject(xhr.response);
};
xhr.send(payload);
}
};
Side note: The content of your initial if
body and the final else
appear to be identical. I think I'd recast the entire onload
:
xhr.onload = function() {
if (xhr.readyState === 4) {
// It's done, what happened?
if (xhr.status === 200) {
if (xhr.getResponseHeader('content-type') !== 'text/html') {
try {
response = JSON.parse(xhr.response);
deferred.resolve(response);
} catch (e) {
deferred.reject(e);
}
} else {
// Something went wrong?
deferred.reject(e);
}
} else if (xhr.status === 0) {
// retry
if (retries--) { // *** Recurse if we still have retries
run();
} else {
// *** Out of retries
deferred.reject(e);
}
}
}
};
Re your comment:
This does resolve my current problem but is there a way to resolve all the promises which are added to call stack if any one of those is resolved?
Yes: To do that with Angular's $q
(I assume that's what you're using), you can just pass the promise you get back from the recursive call into resolve
on your deferred object: Since it's a promise, the deferred will wait for it to be settled and resolve or reject based on what that promise does. If you do this at every level in the chain, the resolutions work their way up the chain:
angular.module("mainModule", []).controller(
"mainController",
function($scope, $q, $http) {
test(true).then(function() {
test(false);
});
function test(flag) {
log(flag ? "Testing resolved" : "Testing rejected");
return recursive(3, flag)
.then(function(arg) {
log("Resolved with", arg);
})
.catch(function(arg) {
log("Rejected with", arg);
});
}
function recursive(count, flag) {
log("recursive(" + count + ", " + flag + ") called");
var d = $q.defer();
setTimeout(function() {
if (count <= 0) {
// Done, settle
if (flag) {
log("Done, resolving with " + count);
d.resolve(count);
} else {
log("Done, rejecting with " + count);
d.reject(count);
}
} else {
// Not done, resolve with promise from recursive call
log("Not done yet, recursing with " + (count - 1));
d.resolve(recursive(count - 1, flag));
}
}, 0);
return d.promise;
}
}
);
function log() {
var p = document.createElement('pre');
p.appendChild(
document.createTextNode(
Array.prototype.join.call(arguments, " ")
)
);
document.body.appendChild(p);
}
pre {
margin: 0;
padding: 0;
}
<div ng-app="mainModule">
<div ng-controller="mainController"></div>
</div>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
Implement javascript promise or async/await
Return a promise in makeCall so you can wait for it in your main method. Here is an example
const makeCall = (call) => {
return new Promise((resolve, reject) => {
myRequest.open('POST', 'http://111.222.3.444:55555')
myRequest.setRequestHeader('Content-Type', "application/x-www-form-urlencoded");
myRequest.send(call)
myRequest.onload = () => {
if (myRequest.status === 200) {
parseString(myRequest.responseText, (err, result) => {
dataList.unshift(result);
resolve();
})
} else {
console.log('Something went wrong, status code: ' + myRequest.status)
reject();
}
}
});
}
Then you can wait for it to finish to continue in your main method. You can do that by using promises like:
makeCal(...)
.then(() => your_success_logic)
.catch((e) => your_error_logic);
Or you can use async/await like this:
app.get('/home', async (req, res) => {
await makeCal(...);
res.render('home.ejs', {
name: dataList[0].root.slipped.mail.name
});
});
How do I convert an existing callback API to promises?
Promises have state, they start as pending and can settle to:
- fulfilled meaning that the computation completed successfully.
- rejected meaning that the computation failed.
Promise returning functions should never throw, they should return rejections instead. Throwing from a promise returning function will force you to use both a } catch {
and a .catch
. People using promisified APIs do not expect promises to throw. If you're not sure how async APIs work in JS - please see this answer first.
1. DOM load or other one time event:
So, creating promises generally means specifying when they settle - that means when they move to the fulfilled or rejected phase to indicate the data is available (and can be accessed with .then
).
With modern promise implementations that support the Promise
constructor like native ES6 promises:
function load() {
return new Promise(function(resolve, reject) {
window.onload = resolve;
});
}
You would then use the resulting promise like so:
load().then(function() {
// Do things after onload
});
With libraries that support deferred (Let's use $q for this example here, but we'll also use jQuery later):
function load() {
var d = $q.defer();
window.onload = function() { d.resolve(); };
return d.promise;
}
Or with a jQuery like API, hooking on an event happening once:
function done() {
var d = $.Deferred();
$("#myObject").once("click",function() {
d.resolve();
});
return d.promise();
}
2. Plain callback:
These APIs are rather common since well… callbacks are common in JS. Let's look at the common case of having onSuccess
and onFail
:
function getUserData(userId, onLoad, onFail) { …
With modern promise implementations that support the Promise
constructor like native ES6 promises:
function getUserDataAsync(userId) {
return new Promise(function(resolve, reject) {
getUserData(userId, resolve, reject);
});
}
With libraries that support deferred (Let's use jQuery for this example here, but we've also used $q above):
function getUserDataAsync(userId) {
var d = $.Deferred();
getUserData(userId, function(res){ d.resolve(res); }, function(err){ d.reject(err); });
return d.promise();
}
jQuery also offers a $.Deferred(fn)
form, which has the advantage of allowing us to write an expression that emulates very closely the new Promise(fn)
form, as follows:
function getUserDataAsync(userId) {
return $.Deferred(function(dfrd) {
getUserData(userId, dfrd.resolve, dfrd.reject);
}).promise();
}
Note: Here we exploit the fact that a jQuery deferred's resolve
and reject
methods are "detachable"; ie. they are bound to the instance of a jQuery.Deferred(). Not all libs offer this feature.
3. Node style callback ("nodeback"):
Node style callbacks (nodebacks) have a particular format where the callbacks is always the last argument and its first parameter is an error. Let's first promisify one manually:
getStuff("dataParam", function(err, data) { …
To:
function getStuffAsync(param) {
return new Promise(function(resolve, reject) {
getStuff(param, function(err, data) {
if (err !== null) reject(err);
else resolve(data);
});
});
}
With deferreds you can do the following (let's use Q for this example, although Q now supports the new syntax which you should prefer):
function getStuffAsync(param) {
var d = Q.defer();
getStuff(param, function(err, data) {
if (err !== null) d.reject(err);
else d.resolve(data);
});
return d.promise;
}
In general, you should not promisify things manually too much, most promise libraries that were designed with Node in mind as well as native promises in Node 8+ have a built in method for promisifying nodebacks. For example
var getStuffAsync = Promise.promisify(getStuff); // Bluebird
var getStuffAsync = Q.denodeify(getStuff); // Q
var getStuffAsync = util.promisify(getStuff); // Native promises, node only
4. A whole library with node style callbacks:
There is no golden rule here, you promisify them one by one. However, some promise implementations allow you to do this in bulk, for example in Bluebird, converting a nodeback API to a promise API is as simple as:
Promise.promisifyAll(API);
Or with native promises in Node:
const { promisify } = require('util');
const promiseAPI = Object.entries(API).map(([key, v]) => ({key, fn: promisify(v)}))
.reduce((o, p) => Object.assign(o, {[p.key]: p.fn}), {});
Notes:
- Of course, when you are in a
.then
handler you do not need to promisify things. Returning a promise from a.then
handler will resolve or reject with that promise's value. Throwing from a.then
handler is also good practice and will reject the promise - this is the famous promise throw safety. - In an actual
onload
case, you should useaddEventListener
rather thanonX
.
Converting Native Promise to JQuery's Promise( For IE )
var p = $.ajax({
url: "yoururl",
type: "GET",
dataType: 'json',
xhrFields: {
withCredentials: true
}
});
// use p.then()
Note:
jQuery has a $.JSON()
function directly.
Also, have a look at Promise-Polyfill if you just want promises!
Related Topics
Create a Date With a Set Timezone Without Using a String Representation
Generate a Hash from String in JavaScript
Difference Between Variable Declaration Syntaxes in JavaScript (Including Global Variables)
How to Set File Input Value When Dropping File on Page
Html5 Video - Percentage Loaded
Vue.Js - Add Class to Clicked Button
JavaScript Console.Log() in an iOS Uiwebview
Module.Exports VS Exports in Node.Js
What Is the Significance of the JavaScript Constructor Property
How to Check If File Exists in Jquery or Pure JavaScript
How to Remove Item from Array by Value
Finding the Max Value of an Attribute in an Array of Objects
What Are Alternatives to Document.Write
Padding or Margin Value in Pixels as Integer Using Jquery
How to Determine If ::Before Is Applied to an Element