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);
});