Asynchronous Programming in JS

Part 2. Paradigms & Constructs

Dmitry Nutels
NI Tech Blog
19 min readJul 5, 2017

--

This is the second part of the Asynchronous Programming series of articles. and it is going to talk, in relative depth, about various ways to write asynchronous JavaScript code, their similarities and differences and, more importantly, why they exist.

This article, as well as the ones to follow, is mostly aimed at an beginner — intermediate level developers, seeking some order amidst the async craziness.

First one, Event Loop & Other Animals, dealt with the mechanisms that make execution of asynchronous code, in JavaScript, possible.

Asynchronous Paradigms

First, let’s solidify the tradition of making an outrageous statement at the beginning of an article in this series by making another one:

Do not write asynchronous code!

Clearly, this is a strange thing to say in an article on asynchronous programming, but it will become much less controversial by the end of it. Or, I’ll retract it, in the best tradition of public statements these days. With apology and all.

A Little Primer

If I were to show you the following piece of JavaScript code:

function a() {
print('a');
b();
c();
}
function b() {
print('b');
}
function c() {
print('c');
}
a();

you, and nearly any other JavaScript programmer (or any programmer at all) would have no trouble to predict the outcome (assuming print is a synchronous operation):

>> a 
>> b
>> c

This code represents an example of a synchronous code — one that is expected to run to completion without any part being skipped/delayed/paused.

In the same manner, this code:

function a() {
print('a');
setTimeout(b, 100);
c();
}
function b() {
print('b');
}
function c() {
print('c');
}
a();

would be identified as asynchronous and the expected result is:

>> a 
>> c
>> b

In most cases, you’d make that conclusion due to familiarity with setTimeout and the most fundamental form of asynchronous programming — asynchronous callbacks:

setTimeout(function() {     ... }, 100);

Callbacks

Callbacks probably don’t need an introduction as they are used throughout JavaScript APIs, libraries and frameworks, from DOM to file system API on Node. They are also the most basic form of defining something to run at some point in the future.

The reason for such an ubiquity is simple — defining a function to be passed around allows you to bind it to data or context via closures or function API, which greatly simplifies state management between now and later parts of your program.

Sync or Async

Callbacks, especially in JavaScript, became a synonym for asynchronous programming, but it really is an incorrect notion. Callbacks can be both synchronous and asynchronous.

Synchronous callback is one that is actually invoked during the execution of the function it was passed to.

The most common example is the Array.prototype methods:

users.forEach(user => {
foo(user);
});
bar();

there is absolutely nothing asynchronous about the arrow function passed to forEach. It is executed for every user and the function foo is called for all users before function bar is called upon loop’s completion.

Asynchronous callbacks, on the other hand, are always invoked sometime in the future after the function they were passed to returns:

setTimeout(() => {
foo(user);
}, 100);
bar();

so bar is called before foo.

Of course, in the context of this article we are interested exclusively in the latter.

NOTE: from this point onwards, we’ll refer to asynchronous callbacks as simply callbacks.

Callback Hell

The trivial setTimeout code above doesn’t truly represent the common case of asynchronous callbacks usage. Consider the following, much more characteristic code:

function contactFollowers(name, emailText) {
request.get({
url: `users/${name}/followers`
}, (err, result) => {
if (!err) {
const {body: followers} = result;
const someFollowers = followers.slice(0, 3);
someFollowers.forEach((follower) => {
const {login} = follower;
request.get({
url: `users/${login}`
}, (err, result) => {
if (!err) {
const {body: user} = result;
const {email, hireable} = user;
if (hireable) {
send(email, emailText, (err, ack) => {
if (!err) {
console.log(ack);
} else {
console.error(‘MAIL’, err);
}
});
}
} else {
console.error(‘USER’, err);
}
});
});
} else {
console.error(‘FOLLOWER’, err);
}
});
}
contactFollowers('gaearon', '...'); // Greetings, Dan...

This kind of code is a manifestation of a phenomenon called Callback Hell. Let’s dissect what is so heinous about that innocuous-looking (albeit ugly) piece of code.

Mental Model

The first problem with any asynchronous code is the mismatch between our mental model of asynchronous processes and the actual process. We can very well describe the execution of the process after the fact, because there is a determined order, but we rarely plan the process with regards to asynchrony.

In one of the Node’s earlier tutorials (or was it a book?) there was an analogy, meant to demonstrate how Node’s asynchronous, event loop-backed implementation works. The example was of a cook, dealing with a bunch of tasks in the kitchen: slicing vegetables, keeping track of pots on the stove, perhaps cleaning the work surface as the cooking goes along. It illustrated a set of non-blocking operations, where the cook spends a little time on each one of them, and then goes back to whatever is needed.

While the analogy helped to describe the non-blocking nature of Node paradigm, it really is not a good analogy for authoring asynchronous code (in the cooking analogy — writing recipes), in general.

Consider the recipe (and the mental preparation) the cook above may use. Do you think it is structured in the following manner?

  1. slice the onions
  2. place the pan on the stove
  3. boil the water
  4. slice the carrots
  5. if pan is hot enough add a tablespoon of olive oil
  6. slice the celery
  7. if the oil is hot place the onions in a the pan
  8. if pan was hot before reduce the heat under the pan to not burn the oil
  9. place tomatoes in the boiled water
  10. pan that was too hot is fine
  11. place onions in the pan now

This is, of course, an exaggeration and a better way to describe these possible sequences can be found (and is, in fact, what separates good cooking book from a collection of barely coherent internet recipes), but it is far from easy.

Our asynchronous code often falls into the same trap and ends up looking similar to the pseudo-recipe above (of a variation of pasta sauce, by the way).

Can We Even Tell?

One of the overlooked aspects of asynchronous programming is our ability to actually recognize it as one. If that sounds trivial, here is an example of a code:

function createCustomer(customerData) {
const {userData, addressData} = customerData;
const user = createUser(userData);
const address = createAddress(addressData);
const customer = saveCustomer(user, address);
return customer;
}

Can you really tell, whether the code is asynchronous or synchronous? Certainly looks like the latter.

How about now, within some context?

function createUser(userData) {
return createRequest(‘users / ’, userData);
}
function createAddress(addressData) {
return createRequest(‘addresses / ’, addressData);
}
function saveCustomer(user, address) {
return Promise.all([user, address]).then((user, address, ...rest) => {
return createRequest('customers/', {user, address});
}).catch((err) => {
console.log(err);
});
}
function createCustomer(customerData) {
const {userData, addressData} = customerData;
const user = createUser(userData);
const address = createAddress(addressData);
const customer = saveCustomer(user, address);
return customer;
}
createCustomer({
userData: {
a: 100
},
addressData: {
b: 200
}
}).then((result) => {
console.log(result);
}).catch((err) => {
console.log(err);
});

So, the first step to deciphering that “Do not write asynchronous code!” mantra is made:

RE: Do not write asynchronous code!

1. Make sure your code can be clearly identified as either synchronous or asynchronous.

We’ll continue adding to the list above, and, hopefully, by the end of the article it will make perfect sense.

Pyramid of Doom

The Pyramid of Doom (or arrow pattern), named after the shape parenthesis and indentation form in a callback-heavy code, is often cited as the main representation and the main problem — ugly, hard-to-read, structurally fragile code.

The code above certainly supports that notion, but the Pyramid of Doom is a relatively benign symptom of the Callback Hell.

That particular piece of code (but not necessarily all such code) can be, relatively easily, restructured into a much more readable, and less-triangular, one:

function contactFollowers(name, emailText, cb) {
request.get({
url: `users/${name}/followers`
}, (err, result) => {
if (!err) {
const {body: followers} = result;
const someFollowers = followers.slice(0, 3);
someFollowers.forEach((follower) => {
getFollowerInfo(follower, emailText, cb);
});
} else {
cb('FOLLOWER', err);
}
});
}
function sendEmail(email, emailText, cb) {
send(email, emailText, (err, ack) => {
if (!err) {
console.log(ack);
} else {
cb('MAIL', err);
}
});
}
function getFollowerInfo(follower, emailText, cb) {
const {login} = follower;
request.get({
url: `users/${login}`
}, (err, result) => {
if (!err) {
const {body: user} = result;
const {email, hireable} = user;
if (hireable) {
sendEmail(email, emailText, cb);
}
} else {
cb('USER', err);
}
});
}
contactFollowers('gaearon', '...', (type, err, ack) => {
if (!err) {
console.log(ack);
} else {
console.error(type, err);
}
});

Does it mean that we are now out of the Callback Hell? Not by a long shot!

However, the fact that flattening doesn’t solve all callback issues shouldn’t stop you from trying to reduce the nesting (and by extension — code complexity).

RE: Do not write asynchronous code!

1. Make sure your code can be clearly identified as either synchronous or asynchronous

2. Attempt to reduce nesting where possible

Keep in mind, when contemplating whether to invest into such an activity, that a refactoring of this kind normally comes with a hidden bonus of a more modular, testable code.

Data Propagation & Error Handling

The nesting reduction attempt above, while commendable and beneficial, didn’t help all that much. Moreover, it introduced new problems:

  1. the necessity to propagate all the input through the entire former pyramid
  2. awkward error handling, where you have to add meta-information about the error to properly handle it at the top level (which otherwise wouldn’t know where the error originated)

Some of these are solvable, on case-by-case basis, but they always a concern. And if you go to the extreme:

function success(ack) {
console.log(ack);
}
function userError(err) {
console.error('USER', err);
}
function followerError(err) {
console.error('FOLLOWER', err);
}
function mailError(err) {
console.error('MAIL', err);
}
function contactFollowers(name, emailText, callbacks) {
request.get({
url: `users/${name}/followers`
}, (err, result) => {
if (!err) {
const {body: followers} = result;
const someFollowers = followers.slice(0, 3);
someFollowers.forEach((follower) => {
getFollowerInfo(follower, emailText, callbacks);
});
} else {
callbacks.followerError(err);
}
});
}
function getFollowerInfo(follower, emailText, callbacks) {
const {login} = follower;
request.get({
url: `users/${login}`
}, (err, result) => {
if (!err) {
const {body: user} = result;
const {email, hireable} = user;
if (hireable) {
sendEmail(email, emailText, (err, acj));
}
} else {
callbacks.userError(err);
}
});
}
function sendEmail(email, emailText, callbacks) {
send(email, emailText, (err, ack) => {
if (!err) {
callbacks.success(ack);
} else {
callbacks.mailError(err);
}
});
}
contactFollowers('gaearon', '...', {success, userError, followerError, mailError});

Now the saying “Callback Hell” makes even more sense. Not only callbacks are hard to deal with, they have a tendency of propagating throughout your code. You fed the mogwai after midnight — now you have to deal with a bunch of vicious gremlins! And you won’t win or survive it.

RE: Do not write asynchronous code!

1. Make sure your code can be clearly identified as either synchronous or asynchronous

2. Attempt to reduce nesting where possible

3. Avoid creating a context (in a form of callback or data) to be passed through

Thrown Exceptions

But wait, it gets better.

Imagine we decided to throw an exception (in this case, when encountering a non-hireable follower):

function mailError(err) {
throw new Error('Lazy bum!');
}

Alas… there is no one to actually catch that error outside of mailError function itself — so now your callbacks can’t throw errors, which is an absolutely fundamental language feature. Or rather — they can, but it will collapse your entire process.

There are ways to mimic the error-throwing behavior, but they are often awkward or rely on non-native hacks.

Contract

All of the above are very common problems that can be avoided or mitigated to an extent with some careful planning, design and proper code organisation.

There is, however, one additional issue that may be much harder to overcome.

Consider a sendEmail function from above — one that sends an email and then provides, via callback, an error upon failure or an acknowledgement upon success. In most cases, such a function is not something you’d write yourself, rather a library created by someone else, perhaps installed using npm install --save send-mail.

function sendEmail(email, emailText, callbacks) {
send(email, emailText, (err, ack) => {
if (!err) {
callbacks.success(ack);
} else {
callbacks.mailError(err);
}
});
}

We’ve provided the sendEmail function with a callback, to literally call us back when done. That’s an inversion of control pattern, of course, and sendEmail and our code have a contract — when its done, either successfully or with an error — it’s going to invoke the provided callback correctly.

That’s quite a bit of a leap of faith. What happens if there is a bug in send-mail library and the callback is invoked in reverse? We get a positive acknowledgement despite mail not being sent? Best case scenario — we have an incorrect log record. Worst? Anything from corrupted data state to a potential lawsuit, if we were legally obligated to send the mail, for example.

What happens if the callback is invoked more than one time per sendEmail invocation? Or none? What if it wasn’t a relatively minor mail delivery issue, but rather an actual monetary transaction?

The instinct, at this point, is to go and write defensive code to take back that control:

function sendEmail(email, emailText, callbacks) {
send(email, emailText, (err, ack) => {
if (!err && (typeof ack === 'string')) {
callbacks.success(ack);
} else if (err instanceof Erorr) {
callbacks.mailError(err);
}
});
}
  • Make sure the callback is called once only
function sendEmail(email, emailText, callbacks) {
let handled = false;
send(email, emailText, (err, ack) => {
if (!handled) {
handled = true;
if (!err && (typeof ack === 'string')) {
callbacks.success(ack);
} else if (err instanceof Erorr) {
callbacks.mailError(err);
}
}
});
}
  • Plan ahead for the case where callback is never called
function sendEmail(email, emailText, callbacks) {
let handled = false;
const timeout = setTimeout(() => {
if (!handled) {
rollbackEverything(...);
}
}, 1000);
send(email, emailText, (err, ack) => {
if (!handled) {
handled = true;
clearTimeout(timeout);
if (!err && (typeof ack === 'string')) {
callbacks.success(ack);
} else if (err instanceof Erorr) {
callbacks.mailError(err);
}
}
});
}
  • Protect against callback being invoked synchronously
function sendEmail(email, emailText, callbacks) {
let handled = false;
const timeout = setTimeout(() => {
if (!handled) {
handled = true;
rollbackEverything(...);
}
}, 1000);
send(email, emailText, (err, ack) => {
if (!handled) {
handled = true;
clearTimeout(timeout);
if (!err && (typeof ack === 'string')) {
callbacks.success(ack);
} else if (err instanceof Erorr) {
callbacks.mailError(err);
}
}
});
}
if (!handled) {
// some sync code doing something BEFORE callback is called
}

This one is harder to illustrate, but imagine there is some code at the end of sendEmail function that actually relies on the callback being asynchronous:

This is a horrible code to create — one that makes whoever comes after you question your sanity and tell his or her friends about “this dude at work that had no idea”. Don’t be that dude.

RE: Do not write asynchronous code!

1. Make sure your code can be clearly identified as either synchronous or asynchronous

2. Attempt to reduce nesting where possible

3. Avoid creating a context (in a form of callback or data) to be passed through

4. …I have nothing … there is nothing to take from that horror of a code above

Promises

The list of issues that plague callbacks, as we’ve seen above, is long, but the last part is especially jarring. Having confidence in your language constructs is imperative to the language being actually used.

For example, the forEach Array method loop:

[1, 2, ..., 100].forEach((item) => {
...
});

guarantees us that the code in the loop is going to get executed the correct amount of times.

Enter Promises. And the name fits.

What is a Promise?

A promise is merely a first-class object with a value to be provided in the future as an eventual result of an operation.

It is a re-inversion (return?) of control, where “foreign” code provides you with a “control handle” to be, eventually, set to contain a value you are interested in. Or an error you might need to handle.

Promises promise the following:

  • any promise is guaranteed to be asynchronous
  • any promise is guaranteed to be resolved only once
  • the asynchronous operation starts when the Promise is created
  • currently client can not cancel or interrupt the operation behind the promise in any way
  • errors thrown in non-error handling callbacks are correctly propagated

Let’s illustrate some of the more interesting ones.

Salvation from the Callback Hell

The following is a list of “promises” of promises.

Any Promise is Guaranteed to be Asynchronous

Promises can be processed in two ways: fulfilled or rejected. Regardless of whether either happens immediately, it is always asynchronous.

ATTENTION: Promise value can not be consumed synchronously, even if it is “immediately” fulfilled or rejected.

So all the following 4 code snippets:

// 1.console.log('a');new Promise((resolve, reject) => {
setTimeout(() => {
resolve('b');
}, 100);
}).then((value) => {
console.log(value);
});
console.log('c');// 2.console.log('a');Promise.resolve('b').then((value) => {
console.log(value);
});
console.log('c');// 3.console.log('a');new Promise((resolve, reject) => {
setTimeout(() => {
reject('b');
}, 100);
}).catch((value) => {
console.log(value);
});
console.log('c');// 4.console.log('a');Promise.reject('b').catch((value) => {
console.log(value);
});
console.log('c');

will print the same result:

>> a 
>> c
>> b

Any Promise is Resolved Only Once

This is as simple as it sounds. Even if the code within the Promise calls resolve or reject multiple times — we are guaranteed to only get one of them — the first.

function foo() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject(100);
resolve(100);
}, 100);
});
}
foo().then((value) => {
console.log(value);
}).catch((err) => {
console.error(err);
});
>> error : 100

Asynchronous Operation Starts When the Promise is Created

The simplest way to observe is to use some sort of network monitoring tool. Pasting the following code into Chrome console should demonstrate this:

fetch('https://api.github.com/users/gaearon');

The request to the GitHub API is issued and responded with nary a then, attached to the promise returned from fetch, in sight.

An interesting corollary to that is that once Promise is in a “processed” state, either by resolution or rejection, it will remain that way and is essentially immutable. Since then does not affect the Promise in any way — you can attach as many of them as you’d like:

new Promise((resolve, reject) => {
resolve(100);
}).then((value) => {
console.log(value);
return value;
}).then((value) => {
console.log(value);
}).catch((err) => {
console.error(err);
});

resulting in 100 being printed twice. As long as then returns a value, it’s going to be propagated..

You can attach as many catch as you’d like too, but only the first one is going to get called unless you return a rejected Promise:

new Promise((resolve, reject) => {
reject(100);
}).catch((value) => {
console.error(value);
return value;
}).catch((value) => {
console.error(value););

producing:

>> 100

and

new Promise((resolve, reject) => {
reject(100);
}).catch((value) => {
console.error(value);
return Promise.reject(value);
}).catch((value) => {
console.error(value);
});

producing:

>> 100 
>> 100

Error Handling

The following code:

new Promise((resolve, reject) => {
setTimeout(() => {
resolve('error');
}, 100);
}).then((value) => {
throw new Error(value);
}).catch((err) => {
console.error(err);
});

properly propagates the error to the catch callback.

These, however:

new Promise((resolve, reject) => {
setTimeout(() => {
reject('error');
}, 100);
}).catch((err) => {
throw new Error(value);
});
try {
new Promise((resolve, reject) => {
setTimeout(() => {
reject('error');
}, 100);
}).catch((err) => {
throw new Error(value);
});
} catch (err) {
console.error(err);
}

still do not work, as one would expect.

Another Way to “Listen” to Promises

The full API for Promises, used relatively infrequently looks like this:

new Promise((resolve, reject) => {
setTimeout(() => {
resolve('error');
}, 100);
}).then((value) => {
console.log(value);
}, (err) => {
console.error(err);
});
new Promise((resolve, reject) => {
setTimeout(() => {
reject('error');
}, 100);
}).then((value) => {
console.log(value);
}, (err) => {
console.error(err);
});

However, if then callback throws — no one is there to handle unless you attach another catch:

new Promise((resolve, reject) => {
setTimeout(() => {
resolve(100);
}, 100);
}).then((value) => {
throw new Error(value);
}, (err) => {
console.error(1, err.message);).catch((err) => {
console.error(2, err.message);
});
new Promise((resolve, reject) => {
setTimeout(() => {
reject(100);
}, 100);
}).then((value) => {
console.log(1, value);
}, (err) => {
console.error(2, err.message);
throw new Error(err.message);
}).catch((err) => {
console.error(3, err.message);
});
});

which, in most cases, is unnecessary.

ATTENTION: do not use promise.then(success, error) form unless you expect the initial error handler to throw as well and know what to do if it does.

As we can see — Promises do indeed address most of the issues we’ve had with callbacks. They also introduce new ways to handle asynchronous flows.

Generators

What is it, Really?

Before anything else, as the name implies, generators generate values. Consider the age-old Fibonacci sequence generation (duh!) question.

If we were to create an upper-bound Fibonacci sequence generator, without actually using generators, we’d write something like this:

function createFib(max) {
let last = -1,
current = 1;
return function() {
[last, current] = [
current, last + current
];
if (current > max) {
return;
}
return current;
};
}
const fib = createFib(10);console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
>> 0
>> 1
>> 1
>> 2
>> 3
>> 5
>> 8
>> undefined

which would work fine.

NOTE the super-awesome swapping of variable values using destructuring.

Using generators, the same code would look like this:

function * createFib(max) {
let last = -1,
current = 1;
while (true) {
[last, current] = [
current, last + current
];
if (current > max) {
return;
}
yield current;
}
}
const it = createFib(10);function fib() {
const {value} = it.next();
return value;
}
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());
console.log(fib());

Not really better, more concise or more semantic. But wait, we can do a little better:

function * createFib(max) {
let last = -1,
current = 1;
while (true) {
[last, current] = [
current, last + current
];
if (current > max) {
return;
}
yield current;
}
}
for (let value of createFib(10)) {
console.log(value);
}

Yep. Iterator is iterable and it unwraps the value for us as well.

So, you can pause the execution of code within the generator (and finally get to use that awesome while(true) loop).

You can pass values to the generator too:

function * sayHelloTo() {
return `Hello, ${yield}!`;
}
const hello = sayHelloTo();
hello.next();
console.log(hello.next('world'));

which is not terribly useful in this case.

Note how the iterator returns a done: true indication — when it’s out of values. Otherwise it would be done: false, of course.

Asynchronous Values

Since async programming is the topic of this series, the topic of asynchrony was bound to come up here as well. Is there some kind of generator-driven construct that would somehow improve the experience?

function getSingleUser(name) {
request.get({
url: `users/${name}`
}, (err, {user}) => {
it.next(user);
});
}
function * getUsers(name) {
const user = yield getSingleUser(name);
console.log(user);
}
const it = getUsers('gaearon');
it.next();

Indeed, that’s a much better experience. Let’s take it further:

function getSingleUser(name) {
request.get({url: `users/${name}`}).then((user) => {
it.next(user);
}).catch((err) => {
it.throw(err);
});
}
function * getUser(name) {
try {
const user = yield getSingleUser(name);
console.log(user);
} catch (err) {
console.error(err);
}
}
const it = getUser('gaearon');
it.next();

The highlighted part of the code looks absolutely synchronous (we also sneaked Promises into the getUser function), including error handling!

The point here is that generators allow us to break asynchronous chain — where we can reduce asynchronous code (or at least one that actually looks asynchronous) to a minimum.

Generated Promises

The code above have several issues, but one should really jump at you — the fact that the getSingleUser function has to close on the iterator. That is both ugly and hurts portability and composability of the code. The inversion of control we fought so hard for with Promises has been reversed once more. We provide our library function (getUser) with what essentially amounts to a callback.

Can we somehow reverse that too? Well, yes.

function * getUser(name) {
try {
const user = yield request.get({url: `users/${name}`});
console.log(user);
} catch (err) {
console.error(err);
}
}
const it = getUser('gaearon');const {value: promise} = it.next();promise.then((value) => {
it.next(value);
}).catch((err) => {
it.throw(err);
});

if we provided an abstraction over the last part:

function * getUser(name) {
try {
const user = yield request.get({url: `users/${name}`});
console.log(user);
} catch (err) {
console.error(err);
}
}
const it = getUser('gaearon');execute(it);

Multiple Promises

it becomes even better, considering there is nothing non-generic in that execute function and it can be used for all generator executions.

Clearly, the example above is a trivial one and asynchronous chains are usually more complex. Let’s try creating one — a function that checks who, between two GitHub users, has more followers and returns true/false:

function * hasMoreFollowers(nameA, nameB) {
const userA = yield request.get({url: `users/${nameA}`});
const userB = yield request.get({url: `users/${nameB}`});
return (userA.followers > userB.followers)
}
function execute(it) {
function executeStep(value) {
const next = it.next(value);
if (next.done) {
return next.value;
} else {
return next.value.then(executeStep);
}
}
const initial = it.next();
return initial.value.then(executeStep);
}
const start = process.hrtime();execute(hasMoreFollowers('isaacs', 'gaearon')).then((better) => {
const elapsed = process.hrtime(start);
console.log(elapsed[0] * 1e3 + elapsed[1] / 1e6);
console.log(better);
}).catch((err) => {
console.error(err);
});

The execute function is now more complex, but what it really does is to run the generator to completion. What’s important, however, is that there is an issue with hasMoreFollowers function.

Can you spot it?

Currently requests for followers are issued in sequence, the second one isn’t executed before the first one is resolved. On my machine the whole process (from purple line to purple line) took around 1200 ms ± 100ms.

A change, that in synchronous code would be inconsequential, takes it to nearly half of that — 700ms ± 50ms:

While the example above we certainly didn’t need the sequential execution, in some cases we do and the execute function + generators provide us with the ability to do so:

function * hasMoreFollowers(nameA, nameB) {
const userAPromise = request.get({url: `users/${nameA}`});
const userBPromise = request.get({url: `users/${nameB}`});
const userA = yield userAPromise;
const userB = yield userBPromise;
return (userA.followers > userB.followers)
}

or, in an even simpler form:

function * hasMoreFollowers(nameA, nameB) {
const results = yield Promise.all([
request.get({url: `users/${nameA}`}),
request.get({url: `users/${nameB}`})
]);
return (results[0].followers > results[1].followers)
}

async/await

= Generated Promises?

Let’s get back to the following piece of code:

function * getUser(name) {
try {
const user = yield request.get({url: `users/${name}`});
console.log(user);
} catch (err) {
console.error(err);
}
}
const it = getUser('gaearon');execute(it);

and recall that execute is:

function execute(it) {
const {value: promise} = it.next();
promise.then((value) => {
it.next(value);
}).catch((err) => {
it.throw(err);
});
}

and ask ourselves: “why all this can’t be yield’s job”? After all, all it does is unwrapping of a Promise closed within yield-ed Promise.

The answer is, in ES7+ or thereabouts, it can and it does:

async function getUser(name) {
try {
const user = await request.get({url: `users/${name}`});
console.log(user);
} catch (err) {
console.error(err);
}
}
getUser('gaearon');

It’s just called async/await.

Multiple Promises

The parallels don’t stop there, our improved followers comparison example from above can also be re-written in async/await:

async function hasMoreFollowers(nameA, nameB) {
const results = await Promise.all([
request.get({url: `users/${nameA}`}),
request.get({url: `users/${nameB}`})
]);
return (results[0].followers > results[1].followers)
}
hasMoreFollowers('isaacs', 'gaearon').then((better) => {
console.log(better);
}).catch((err) => {
console.error(err);
});

Finally, we made it to the latest piece of the puzzle:

RE: Do not write asynchronous code!

1. Make sure your code can be clearly identified as either synchronous or asynchronous

2. Attempt to reduce nesting where possible

3. Avoid creating a context (in a form of callback or data) to be passed through

4. Consider using generators/async/await to break asynchronous chain

Summary

It is clear, after diving a little deeper, that the problem is not with asynchronous code, rather with our ability to create and comprehend one. One of the techniques is to minimise the amount of asynchronous code and separate it into clearly distinguishable “sub-section” of your code.

When combined with SOLID principles, you should be able to create well-balanced, semantic and maintainable code.

In the next (and last) part of the series we’ll go over some code examples that illustrate the paradigms discussed in the earlier parts.

Happy async-awaiting.

Originally published at blog.naturalint.com on July 5, 2017.

--

--