I want to work with promises but I have a callback API in a format like:
window.onload; // set to callback
...
window.onload = function() {
};
function request(onChangeHandler) {
...
}
request(function() {
// change happened
...
});
function getStuff(dat, callback) {
...
}
getStuff("dataParam", function(err, data) {
...
})
API;
API.one(function(err, data) {
API.two(function(err, data2) {
API.three(function(err, data3) {
...
});
});
});
Promises have state, they start as pending and can settle to:
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.
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();
}
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.
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
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:
.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. onload
case, you should use addEventListener
rather than onX
.