ES6 Callbacks, Promises, and Generators
This is the part of a series of blogs on the new features on the upcoming ECMAScript 6 (ES6) specification which JavaScript implements. However this post will not be as focused on ES6 as my other posts on ES6 as we aim to also cover Callbacks and Promises.
JavaScript has a single threaded runtime, at least for now. So it makes use of a single thread to fulfill all of the application’s work. This is why its asynchronous property allows you to start something that completes later on. This is useful for handling work that would require a lot of processing time such as calls to a webservice, waiting for a CSS transition, or a timeout timer.
Callback #
Callbacks are a way to handle the load of having to deal with a long running process. Here we execute another function after we get the result from calling an AJAX call. The Callback Hell happens when we keep nesting callbacks upon callbacks until the callback tree just becomes unmanageable with no common pattern.
Callback Hell:
$.ajax({
type: "GET",
url: "test.json",
success: function(data) {
// do something
$.ajax({
type: "GET",
url: "test2.json?id="+data.id,
success: function(data2) {
// More callback hell
},
error: function(xhr, status, error) {
$("#errorBar2").append(
"<span>"+error.toString()+"</span>");
}
});
},
error: function(xhr, status, error) {
$("#errorBar").append(
"<span>"+error.toString()+"</span>");
}
});
A number of things is wrong with this example such as repeating the code for error situations as well as having crazy nested functions. Fortunately there is a more elegant way of doing callbacks so that it makes it easier for other devs to read your code.
Cleaner Callback Hell:
$.ajax({
type: "GET",
url: "test.json",
success: testTwoWithId,
error: handleError
});
function testTwoWithId(data) {
// do something
$.ajax({
type: "GET",
url: "test2.json?id="+data.id,
success: anotherFreakingFunction,
error: handleError
});
}
function anotherFreakingFunction(dataAgain) {
// do something again
}
function handleError(error) {
$("#errorBar").append("<span>"+error.toString()+"</span>");
}
Promises - sometimes called Deferred #
A Promise represents a future value. It is the standard syntax for anything that has a delayed response. Most frameworks have something similar to Promises (or a Deferred object) which implements resolve
or reject
functions. The Promise object is then “then-able” which would respond to resolve
or reject
events depending on the result. It sounds pretty similar to a traditional Callback function but where Promises make sense is when you have nested Promises/Callbacks. Promises gives you a nice abstraction to manage the complexity.
var test = $.ajax({
type: "GET",
url: "test.json"
});
test.then(function(data) {
return $.get("test2.json?id=" + data.id);
}).then(function(dataAgain) {
var pos = sampleFunction(dataAgain);
return $.get("test2.json?id=" + dataAgain.arr[pos].id)
}).then(anotherFreakingFunction(dataYetAgain), function(xhr, status, error) {
// Handle errors for any of the actions
handleError(error);
});
function anotherFreakingFunction(dataYetAgain) {
console.log(dataYetAgain);
}
function handleError(error) {
$("#errorBar").append("<span>"+error.toString()+"</span>");
}
Promise Flow:
- The
$.ajax
function generates a jQuery Promise object - Inside
$.ajax
function when it has completed the webservice call then it calls eitherresolve
orreject
functions to the Promise object - If it calls the
resolve
function then we enter thethen
clause - Otherwise if it goes to the
reject
function then it goes to (in this case)handleError
function
Generators (ES6 only) #
Functions that are generators are denoted passed by a star *
such as function*()
. We can return results using the yield
keyword. The function stops invoking at the yield
and then resumes again at the next invocation of the next()
call from where we last stopped.
Run..Stop..Run
Generator Basics:
var genFunc = function*() {
var value = yield 1;
value; // becomes {test: "testy"} after the second next function is invoked
yield 2;
}
var gen = genFunc();
gen.next(); // {value: 1, done: false}
gen.next({test: "testy"}); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}
We can use generators to completely give our Promises structure a nice standard layout of easily understandable async js. The resolved value of $.get()
gets sent to var data, dataAgain, dataYetAgain
at their respective times when the async code has completed. We also stop just before each yield
call so we get the data in order.
// Using Bluebird for coroutine
Promise.coroutine(function* () {
var data = yield $.get("test.json");
// run code that uses data
var dataAgain = yield $.get("test2.json?id="+data.id);
// run code that uses dataAgain
var dataYetAgain = yield $.get("test2.json?id=" + dataAgain.arr[pos].id);
// run code that uses dataYetAgain
console.log(data, dataAgain, dataYetAgain);
})().catch(function(error) {
handleError(error);
});
Javascript generators allows you to structure your code seemingly like synchronously, but behind the scenes they work asynchronously.