How do I promisify native XHR?
Solution 1
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).
Solution 2
This could be as simple as the following code.
Keep in mind that this code will only fire the reject
callback when onerror
is called (network errors only) and not when the HTTP status code signifies an error. This will also exclude all other exceptions. Handling those should be up to you, IMO.
Additionally, it is recommended to call the reject
callback with an instance of Error
and not the event itself, but for sake of simplicity, I left as is.
function request(method, url) {
return new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.open(method, url);
xhr.onload = resolve;
xhr.onerror = reject;
xhr.send();
});
}
And invoking it could be this:
request('GET', 'http://google.com')
.then(function (e) {
console.log(e.target.response);
}, function (e) {
// handle errors
});
Solution 3
For anyone who searches for this now, you can use the fetch function. It has some pretty good support.
fetch('http://example.com/movies.json')
.then(response => response.json())
.then(data => console.log(data));
I've firstly used @SomeKittens's answer, but then discovered fetch
that does it for me out of the box :)
Solution 4
I think we can make the top answer much more flexible and reusable by not having it create the XMLHttpRequest
object. The only benefit of doing so is that we don't have to write 2 or 3 lines of code ourselves to do it, and it has the enormous drawback of taking away our access to many of the API's features, like setting headers. It also hides properties of the original object from the code that's supposed to handle the response (for both successes and errors). So we can make a more flexible, more widely applicable function by just accepting the XMLHttpRequest
object as input and passing it as the result.
This function converts an arbitrary XMLHttpRequest
object into a promise, treating non-200 status codes as an error by default:
function promiseResponse(xhr, failNon2xx = true) {
return new Promise(function (resolve, reject) {
// Note that when we call reject, we pass an object
// with the request as a property. This makes it easy for
// catch blocks to distinguish errors arising here
// from errors arising elsewhere. Suggestions on a
// cleaner way to allow that are welcome.
xhr.onload = function () {
if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
reject({request: xhr});
} else {
resolve(xhr);
}
};
xhr.onerror = function () {
reject({request: xhr});
};
xhr.send();
});
}
This function fits very naturally into a chain of Promise
s, without sacrificing the flexibility of the XMLHttpRequest
API:
Promise.resolve()
.then(function() {
// We make this a separate function to avoid
// polluting the calling scope.
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/');
return xhr;
})
.then(promiseResponse)
.then(function(request) {
console.log('Success');
console.log(request.status + ' ' + request.statusText);
});
catch
was omitted above to keep the sample code simpler. You should always have one, and of course we can:
Promise.resolve()
.then(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
return xhr;
})
.then(promiseResponse)
.catch(function(err) {
console.log('Error');
if (err.hasOwnProperty('request')) {
console.error(err.request.status + ' ' + err.request.statusText);
}
else {
console.error(err);
}
});
And disabling the HTTP status code handling doesn't require much change in the code:
Promise.resolve()
.then(function() {
var xhr = new XMLHttpRequest();
xhr.open('GET', 'https://stackoverflow.com/doesnotexist');
return xhr;
})
.then(function(xhr) { return promiseResponse(xhr, false); })
.then(function(request) {
console.log('Done');
console.log(request.status + ' ' + request.statusText);
});
Our calling code is longer, but conceptually, it's still simple to understand what's going on. And we don't have to rebuild the entire web request API just to support its features.
We can add a few convenience functions to tidy up our code, as well:
function makeSimpleGet(url) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
return xhr;
}
function promiseResponseAnyCode(xhr) {
return promiseResponse(xhr, false);
}
Then our code becomes:
Promise.resolve(makeSimpleGet('https://stackoverflow.com/doesnotexist'))
.then(promiseResponseAnyCode)
.then(function(request) {
console.log('Done');
console.log(request.status + ' ' + request.statusText);
});
Solution 5
jpmc26's answer is quite close to perfect in my opinion. It has some drawbacks, though:
- It exposes the xhr request only until the last moment. This does not allow
POST
-requests to set the request body. - It is harder to read as the crucial
send
-call is hidden inside a function. - It introduces quite a bit of boilerplate when actually making the request.
Monkey patching the xhr-object tackles these issues:
function promisify(xhr, failNon2xx=true) {
const oldSend = xhr.send;
xhr.send = function() {
const xhrArguments = arguments;
return new Promise(function (resolve, reject) {
// Note that when we call reject, we pass an object
// with the request as a property. This makes it easy for
// catch blocks to distinguish errors arising here
// from errors arising elsewhere. Suggestions on a
// cleaner way to allow that are welcome.
xhr.onload = function () {
if (failNon2xx && (xhr.status < 200 || xhr.status >= 300)) {
reject({request: xhr});
} else {
resolve(xhr);
}
};
xhr.onerror = function () {
reject({request: xhr});
};
oldSend.apply(xhr, xhrArguments);
});
}
}
Now the usage is as simple as:
let xhr = new XMLHttpRequest()
promisify(xhr);
xhr.open('POST', 'url')
xhr.setRequestHeader('Some-Header', 'Some-Value')
xhr.send(resource).
then(() => alert('All done.'),
() => alert('An error occured.'));
Of course, this introduces a different drawback: Monkey-patching does hurt performance. However this should not be a problem assuming that the user is waiting mainly for the result of the xhr, that the request itself takes orders of magnitude longer than setting up the call and xhr requests not being sent frequently.
PS: And of course if targeting modern browsers, use fetch!
PPS: It has been pointed out in the comments that this method changes the standard API which can be confusing. For better clarity one could patch a different method onto the xhr object sendAndGetPromise()
.
Related videos on Youtube
SomeKittens
#!/bin/sh echo $GREETING, I am $NAME, working at $WORKPLACE in $LANGUAGE. echo $WITTY_COMMENT; Complaints, rants, comments and/or fanmail? Contact Me I also run InMailFail
Updated on January 23, 2022Comments
-
SomeKittens over 2 years
I want to use (native) promises in my frontend app to perform XHR request but without all the tomfoolery of a massive framework.
I want my xhr to return a promise but this doesn't work (giving me:
Uncaught TypeError: Promise resolver undefined is not a function
)function makeXHRRequest (method, url, done) { var xhr = new XMLHttpRequest(); xhr.open(method, url); xhr.onload = function() { return new Promise().resolve(); }; xhr.onerror = function() { return new Promise().reject(); }; xhr.send(); } makeXHRRequest('GET', 'http://example.com') .then(function (datums) { console.log(datums); });
-
Bergi almost 9 yearsSee also the generic reference How do I convert an existing callback API to promises?
-
PHP Guru over 3 yearsYour makeXHRRequest function has to return a promise
-
-
Bergi about 9 yearsYou might also want to add options for
responseType
, authentication, credentials,timeout
… Andparams
objects should support blobs/bufferviews andFormData
instances -
prasanthv almost 9 yearsWould it be better to return a new Error on reject?
-
Peleg almost 9 years@MadaraUchiha I guess it's the tl;dr version of it. It gives the OP an answer to their question and only that.
-
dqd over 8 yearsAdditionally, it does not make sense to return
xhr.status
andxhr.statusText
on error, since they are empty in that case. -
caub over 8 yearswhere goes the body of it's a POST request?
-
Peleg over 8 years@crl just like in a regular XHR:
xhr.send(requestBody)
-
caub over 8 yearsyes but why didn't you allow that in your code? (since you parametrized the method)
-
mindplay.dk almost 8 years@prasanthv it wouldn't just be better - as @dqd pointed out,
xhr.status
andxhr.statusText
will contain nothing, since there was no (valid) response, so the code sample (as-is) doesn't make sense in this regard; likely, the most meaningful thing we can do, is something likereject(new Error("unable to complete " + opts.method + " request for: " + opts.url))
-
hairbo over 7 yearsThis code seems to work as advertised, except for one thing. I expected that the right way to pass params along to a GET request was via xhr.send(params). However, GET requests ignore any values sent to the send() method. Instead, they just need to be query string params on the URL itself. So, for the above method, if you want the "params" argument to be applied to a GET request, the routine needs to be modified to recognize a GET vs. POST, and then conditionally append those values to the URL that's handed to xhr.open().
-
ayxos about 7 yearsWill this code work with sync calls? Will this create a xhr sync promise?
-
Steve Chamaillard almost 7 yearsI like this answer as it provides very simple code to work with immediately which answers the question.
-
bdesham over 6 yearsOlder browsers don’t support the
fetch
function, but GitHub has published a polyfill. -
James Dunne over 6 yearsI wouldn't recommend
fetch
as it doesn't support cancellation yet. -
sideshowbarker over 6 yearsThe spec for the Fetch API now provides for cancellation. Support has so far shipped in Firefox 57 bugzilla.mozilla.org/show_bug.cgi?id=1378342 and Edge 16. Demos: fetch-abort-demo-edge.glitch.me & mdn.github.io/dom-examples/abort-api. And there are open Chrome & Webkit feature bugs bugs.chromium.org/p/chromium/issues/detail?id=750599 & bugs.webkit.org/show_bug.cgi?id=174980. How-to: developers.google.com/web/updates/2017/09/abortable-fetch & developer.mozilla.org/en-US/docs/Web/API/AbortSignal#Examples
-
sideshowbarker over 6 yearsThe answer at stackoverflow.com/questions/31061838/… has cancelable-fetch code example that so far already works in Firefox 57+ and Edge 16+
-
supersan almost 6 yearsBest answer. Simple and effective. Also for anyone else wondering, to convert the response to JSON, just use
JSON.parse(e.target.response)
-
Matt Montag over 5 yearsCancelable fetch is still giving me trouble in Chrome 69. Using XMLHttpRequest just seems a whole lot easier.
-
heinob about 5 yearsOne should use
resolve(xhr.response | xhr.responseText);
In most browsers the repsonse is in responseText in the meantime. -
jpmc26 about 5 yearsI avoid monkey patching because it's surprising. Most developers expect that standard API function names invoke the standard API function. This code still hides the actual
send
call but also can confuse readers who know thatsend
has no return value. Using more explicit calls makes it clearer that additional logic has been invoked. My answer does need to be adjusted to handle arguments tosend
; however, it's probably better to usefetch
now. -
t.animal about 5 yearsI guess it depends. If you return/expose the xhr request (which seems dubious anyhow) you are absolutely right. However I don't see why one would not do this within a module and expose only the resulting promises.
-
jpmc26 about 5 yearsI'm referring specially to anyone having to maintain the code you do it in.
-
t.animal about 5 yearsLike I said: It depends. If your module is so huge that the promisify function gets lost between the rest of the code you've probably got other problems. If you've got a module where you just want to call some endpoints and return promises I don't see a problem.
-
jpmc26 about 5 yearsI disagree that it depends on the size of your code base. It's confusing to see a standard API function do something other than it's standard behavior.
-
gauguerilla about 4 yearsthis works like a charm! Yet behaviour of cookie handling seems to be different: If wrapped inside the Promise, an Auth Cookie returned from an API is not saved. If I leave everything the same but remove the Promise wrapping, the cookie is written. Any ideas about why that is the case and if I can get around that behaviour?
-
jpaugh almost 4 years@microo8 It would be nice to have a simple example using fetch, and here seems liek a good place to put it.
-
UNdedss about 3 yearsthis should a top-voted answer cause it makes code clean
-
Marcin Wojtach almost 3 yearsIt would be nice to handle all 400 responses in catch block after rejecting the promise, so you should change the if statement to && operator
(xhr.status>=200 && xhr.status<400)
otherwise all 400 responses will fall into the first statement and always be truthy. -
PHP Guru almost 3 yearsIt's a matter of preference. I think that fetch() works the same way though and doesn't reject promises that return 400+ status codes. This makes the most sense to me because technically the promise was fulfilled if the HTTP request receives a response, not rejected, even if that response was a 400+ status code.
-
Marcin Wojtach almost 3 yearsIt does have a lot of sense. And you are right, fetch doesn't reject 400+'s.