Direct style 🔗
var firstUser = getUser({id: 1});
ui.renderUser(firstUser);
Continuation passing style 🔗
getUser({id: 1}, function(firstUser) {
ui.renderUser(firstUser);
});
The function written in CPS receives the 'continuation' as an extra argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument.
async CPS: Very common in JS
runAsyncTask(taskInput, function(taskOutput) {
console.log("task finished with output " + taskOutput);
});
Not all callbacks qualify as continuations
// This is pub-sub, not continuation passing
$('.someButton').on('mouseover', function() {
tooltip.show();
});
A proxy for a value that will become available in the future.
(or that is already available. Or that will never be available. (edge cases))
Here's a promise for a user
var userPromise = $.get('http://api/users/1'); // jQuery 3.0, please
This promise is pending.
A user-promise is not a user
// Let's assume user objects look like {id: '..', name: '..', email: '..'}.
// This is an error
console.log(userPromise.name); // No 'name' attribute on userPromise object
So what can we do with it?
A pending promise will eventually be fulfilled with a value.
So what we can do is specify what should happen when it's fulfilled:
userPromise.then(function(user) {
ui.renderUser(user);
});
Commonly, we omit the promise object and chain then directly onto the initial promise-returning function call
$.get('http://api/users/1').then(function(user) {
ui.renderUser(user);
});
Here's another example.
Assume wait() is a promise-returning function
wait(60000).then(function() {
alert("BOO!");
});
Note: Fullfilment handlers will run even if the promise is already fulfilled
var timeoutPromise = wait(60000);
timeoutPromise.then(function() {
console.log('BOO');
timeoutPromise.then(function() {
console.log('HOO');
});
});
// This will log 'BOO' and then 'HOO'
How is this better than async continuation passing?
// Async CPS
runAsyncTask(taskInput, function(taskOutput) {
console.log("task finished with output " + taskOutput);
});
// vs
// Promise
runAsyncTask(taskInput).then(function(taskOutput) {
console.log("task finished with output " + taskOutput);
});
👎 Callback APIs are inconsistent
$.ajax({
url: 'http://..',
success: function(data, textStatus, jqXhr) {/* use data */},
error: function(jqXHR, textStatus, errorThrown) {/* handle error */}
});
http.request('http://..', function(error, response) {
if (error) {/* handle error */}
/* handle response (no error) */
});
require(['jquery', 'underscore'],
function($, _) {/* make use of $ and _ */},
function(error) {/* handle error */}
);
👍 Promise API
runAsyncTask(taskInput).then(function(taskOutput) {
// Task output = promise's fulfillment value
});
$.ajax({url: 'http://..'}).then(function(data) {
// use data
});
http.request('http://..').then(function(response) {
// handle response
});
require(['jquery', 'underscore']).then(function(deps) {
// make use of deps.$ and deps._
(We'll talk about errors in a bit ..)
👎 Trust issue: No guarantee that callbacks will be treated as continuation
'3rd party' code may 'choose' to invoke given callback multiple times. Or invoke the error callback and the success callback. Both very popular JS bugs. (node example).
👍 Promise guarantee:
When fulfilled, a promise:
(Taken from the spec.)
👎 Trust issue: No guarantee that continuations will be invoked synchronously/asynchronously
Massively popular Stack Overflow question:
I tried to return the value from the success callback as well as assigning the response to a local variable inside the function and return that one, but none of those ways actually return the response.
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
result = response;
// return response; // <- I tried that one as well
}
});
return result;
}
var result = foo(); // It always ends up being `undefined`.
But what if we had this instead?
function foo() {
var result;
getData(options, function(data) {
result = data;
});
return result;
}
var result = foo(); // Now, does this work?
Isaac Z. Schlueter, the node / npm guy has written many wize words on this.
👍 Promise guarantee:
A fulfillment handler must not be called until the execution context stack contains only platform code.
I.e. it must be called asynchronously
(Taken from the spec.)
'You Don't Know JS: Async & Performance' has an entire chapter dissing callbacks.
Nope
Promises offer a way to regain useful control flow patterns only applicable to the direct (synchronous) style.
Promise chaining & transformations
$.get('http://api/users/1')
.then(function(user) {
ui.renderUser(user);
})
.then(function() {
return wait(1000);
})
.then(function() {
ui.renderMessage("You can now edit the user");
});
This works because .then(..)
always returns a promise.
How does chaining work, exactly?
A fullfilment handler may return a value that
is not a promise
$.get('http://api/users/1').then(function(user) {
return user.name;
});
or is a promise
$.get('http://api/users/1').then(function(user) {
return $.get('http://api/organizations/' + user.organization);
});
Fullfilment handler returns value that isn't promise
Then promise
gets fulfilled with that value
// promise p will be fulfilled with the user name
// at the time that the _original promise_ (the user promise) is fulfilled
var p = $.get('http://api/users/1').then(function(user) {
return user.name;
});
Fullfilment handler returns a promise
Then promise
adopts the state of (effectively becomes) the returned promise
// promise p will be fulfilled with the user organization
// at the time that the last API call completes
var p = $.get('http://api/users/1').then(function(user) {
return $.get('http://api/organizations/' + user.organizationId);
});
Let's decompose the original example
var userPromise = $.get('http://api/users/1');
var postRenderPromise = userPromise.then(function(user) {
ui.renderUser(user);
// Implicitly returns undefined. So postRenderPromise is fulfilled
// with undefined 'immediately after' userPromise is fulfilled
});
var pausePromise = postRenderPromise.then(function() {
return wait(1000);
// Returns the promise that wait returns. So pausePromise is pending
// and will be fulfilled '1s after' postRenderPromise
})
var endPromise = pausePromise.then(function() {
ui.renderMessage("You can now edit the user");
// Implicitly returns undefined. So endPromise is fulfilled
// with undefined 'immediately after' pausePromise is fulfilled
});
CPS → (promises) → 'DS', pt 01:
Executing async tasks sequentially, in CPS
AKA the pyramid of doom
// Get the admin of the organization that the 'first user' belongs to
var admin;
getUserById(1, function(user) {
getOrgById(user.orgId, function(org) {
getUserById(org.adminId, function(adminUser) {
admin = adminUser;
});
});
});
Executing async tasks sequentially, in DS
var user = getUserById(1);
var org = getOrgById(user.orgId);
var admin = getUserById(org.adminId);
Executing async tasks sequentially with promises
var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});
(This is more common though:)
var adminPromise = getUserById(1)
.then(function(user) {
return getOrgById(user.orgId);
})
.then(function(org) {
return getUserById(org.adminId);
});
(TL;DR: I doesn't work)
Error handling, in DS
var admin;
try {
var user = getUserById(1);
var org = getOrgById(user.orgId);
admin = getUserById(org.adminId);
} catch (exception) {
ui.renderError(exception);
return;
}
// Continue with happy path, make use of admin reference
Async CPS version. Wouldn't it be nice if
var admin;
try {
getUserById(1, function(user) {
getOrgById(user.orgId, function(org) {
getUserById(org.adminId, function(adminUser) {
admin = adminUser;
});
});
});
} catch (exception) {
ui.renderError(exception);
return;
}
// Continue with happy path, make use of admin reference
sadly, this doesn't work due to the continuations executing (and the exceptions being thrown) within call stacks that aren't the original catch-block-containing call stack.
So we have to do this instead
var admin;
try {
getUserById(1, function(user) {
try {
getOrgById(user.orgId, function(org) {
try {
getUserById(org.adminId, function(adminUser) {
admin = adminUser;
});
} catch (exception) {
ui.renderError(exception);
}
});
} catch (exception) {
ui.renderError(exception);
}
});
} catch (exception) {
ui.renderError(exception);
return;
}
:(
So how do promises facilitate error handling? We've already seen how
A pending promise will eventually be fulfilled with a value.
Well ..
A pending promise will eventually be fulfilled with a value.
A pending promise will either
(or remain pending forever - not interesting)
Enter the 'rejection handler'
userPromise.then(function(user) { // Both fulfillment & rejection handlers
ui.renderUser(user);
}, function(error) {
ui.renderError(error);
});
userPromise.then(null, function(error) { // No fulfillment handler
ui.renderError(error);
});
Chaining & transformations, revisited
var transformedPromise = originalPromise.then(fulHanlder, rejHandler);
transformedPromise
gets resolved with that value: It's either fulfilled
with a returned non-promise value or adopts the state of a returned promise.
transformedPromise
is rejected with that exception (as the reason).
For example
$.get('http://api/users/1')
.then(function(user) {
if (user.name !== 'root') {
throw new Error('Unexpected non-root user');
}
ui.renderUser(user);
})
.then(function() {
return wait(1000);
})
.then(function() {
ui.renderMessage("You can now edit the user");
})
.then(null, function(error) {
ui.renderError(error);
});
Beware!
$.get('http://api/users/1')
.then(function(user) {
if (user.name !== 'root') {
throw new Error('Unexpected non-root user');
}
ui.renderUser(user);
}, function(error) {
// This rejection handler is called if the $.get promise
// is rejected. But is _not_ called for the unexpected-user error
})
.then(null, function(error) {
// This rejection handler is called for _any_ error
});
CPS → (promises) → 'DS', pt 02:
We've already seen (no error handling here):
var user = getUserById(1);
var org = getOrgById(user.orgId);
var admin = getUserById(org.adminId);
var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});
with error handling
var admin;
try {
var user = getUserById(1);
var org = getOrgById(user.orgId);
admin = getUserById(org.adminId);
} catch (exception) {
ui.renderError(exception);
return;
}
// Continue with happy path, make use of admin reference
var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});
admin.then(function(admin) {
// Continue with happy path, make use of admin reference
}, function(error) {
ui.renderError(exception);
})
Creating promises in Q, Bluebird, ES6
Call stacks
Where to go from here