YDKJS Async ,Callbacks, and Promises, Oh My!
As part of the P1XT guide , I’m reading through the YDKJS series (again). This time I am taking down my thoughts and notes, to help me learn and maybe someone else will read it too. If I make any mistakes or misinterpretations, please correct me!
While Types+Grammar was overall concrete with real-world examples, I feel we are drifting off into space with Async+Performance.
So what is Async? In JavaScript, while we expect the program to be running in order this is not always the case. Sometimes there are gaps when the program is waiting for a response from some other “thing” like a server or a user. When using Ajax requests, we don’t get the data back immediately (it takes some time for the server to load etc), for this reason it’s common to use a callback function within the Ajax request, this function will run only when the data is retrieved. The way JS behaves is dictated by the hosting environment i.e. the browser if running client-side, which handles execution over time via an “event loop”. There is a discussion on single-threaded (JS) vs. parallel-threaded, and single-threaded in much easier to write and understand because only one thing is running, and it runs to completion. In parallel-threading you would need to watch out for shared memory among functions. However, JS is non-deterministic which means the output from below code depends if the first or second Ajax request finishes first.
var a = 20;
function foo() {
a = a + 1;
}
function bar() {
a = a * 2;
}
// ajax(..) is some arbitrary Ajax function given by a library
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
Depending on which Ajax call returned first, you could get a==42
or a==41
. This is referred to as a “race condition” and cannot be predicted reliably. If concurrent processes are non-interacting (don’t affect one another), then there is no need to worry about race conditions. But, more often they WILL interact. For example:
var res = [];
function response(data) {
res.push( data );
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
Will the response from url.1
be in res[0]
or res[1]
? We can never tell for sure. Luckily we are clever and can write:
var res = [];
function response(data) {
if (data.url == "http://some.url.1") {
res[0] = data;
}
else if (data.url == "http://some.url.2") {
res[1] = data;
}
}
ajax( "http://some.url.1", response );
ajax( "http://some.url.2", response );
This ensures we know which url is in which part of our data structure, but is sort of a hack-y way to get around the issue.
The next chapter is all about callbacks, which I am not moderately familiar with thanks to FreeCodeCamp. Callbacks have been widely used in JS programming, but I have a feeling Mr. Simpson is going to tear down everything I thought I knew. What I really like about this book so far is the metaphors he uses to describe how JS handles async. The queue-jumping of Jobs and now the multi-tasking or fake multitasking of trying to focus on 1 book helps to understand the otherwise highly abstract concepts of asynchronous JavaScript.
Mr. Simpson introduces the idea of “callback hell” where many callback functions are nested within each other, waiting for different things to happen. Looking at a complicated list of callbacks, you need to jump around in the code to figure out the order which things will happen. A more serious issue using callback functions, one that I was completely unaware of, is security. With callbacks with APIs, you are trusting some third-party with the execution of the callback function (you trust them with the continuation of your program). I think for small projects callbacks are still OK, and a lot of developers still use them, but for larger scale things and moving forward, we need to look at Promises. A promise is knowing that a future-value will be replaced by a value (either success or failure…or no answer/unresolved). This is somewhat explained in a nice cheeseburger-ordering metaphor, which reminds me I haven’t had lunch yet. Using promises, you exchange a “promise-of-a-future-value” for the “value-itself”, once its ready. Promises work by calling a .then()
function after they are resolved. .then
can take two functions as its arguments: the first being on success, the second being on error. .then
returns another promise, with a resolved value equal to whatever you put into the final return
.
var p = Promise.resolve( 21 );
var p3 = p
.then( function(v){
console.log( v ); // 21
// fulfill the chained promise with value `42`
return v * 2;
} )
// here's the chained promise
.then( function(v){
console.log( v ); // 42
return v*2;
} ); //p3 is a promise with resolved value = 84
Promises are immutable object
s, once they have been resolved they cannot be changed (their status set to resolved), which is an important part of why they’re secure. Error handling with promises is straightforward on the surface, because you will get back the “rejected” in your then
function. But what if an error occurs in your fulfilled
? You could lose that error:
var p = Promise.resolve( 42 );
p.then(
function fulfilled(msg){
// numbers don't have string functions,
// so will throw an error
console.log( msg.toLowerCase() );
},
function rejected(err){
// never gets here
}
);
This is what the author refers to as an example of “Pit of Despair” design, where the programmer is punished for an accident. One way to avoid this is to add a catch
to the end of your then
chain, but any error present inside your catch
statement would still be missed. Some libraries include a done
clause which does not return a promise, but it is not part of the ES6 standard.
The Promise.all([...])
method can act as a gate, waiting for 2 or more promises (like Ajax requests) to resolve, before starting on a third action in the then
clause. It doesn’t matter the order p1,p2
finish, only that they are both resolved.
// `request(..)` is a Promise-aware Ajax utility,
// like we defined earlier in the chapter
var p1 = request( "http://some.url.1/" );
var p2 = request( "http://some.url.2/" );
Promise.all( [p1,p2] )
.then( function(msgs){
// both `p1` and `p2` fulfill and pass in
// their messages here
return request(
"http://some.url.3/?v=" + msgs.join(",")
);
} )
.then( function(msg){
console.log( msg );
} );
The promise returned from Promise.all call will receive a fulfillment message that is an array of all fulfillment messages. Another similar, but different, method is Promise.race([...])
which takes an array of promises/thenables, and only waits for one of them to finish before executing the then
.
With the help of https://davidwalsh.name/write-javascript-promises I am determined to revamp my Weather App to use Promises!!
EDIT: Here it is, instead of callbacks I used then
to execute each step in order. I suppose I could have abstracted a bit more on the code.