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