“I am delighted to support Nicolás’ endeavor because his book looks exactly like what people who are coming to JavaScript with fresh eyes need.”
– Brendan Eich
Ideal for professional software developers with a basic understanding of JavaScript, this practical book shows you how to build small, interconnected ES6 JavaScript modules that emphasize reusability. You’ll learn how to face a project with a modular mindset, and how to organize your applications into simple pieces that work well in isolation and can be combined to create a large, robust application.
This book focuses on two aspects of JavaScript development: modularity and ES6 features. You’ll learn how to tackle application development by following a scale-out approach. As pieces of your codebase grow too big, you can break them up into smaller modules.
The book can be read online for free or purchased through Amazon.
This book is part of the Modular JavaScript series.
Start with the book series launch announcement on Pony Foo
Participate in the crowdfunding campaign on Indiegogo
Amplify the announcement on social media via Thunderclap
Share a message on Twitter or within your social circles
Contribute to the source code repository on GitHub
Read the free HTML version of the book on Pony Foo
Purchase the book from O’Reilly on Amazon
Chapter 4
Iteration and Flow Control
Having covered the essential aspects of ES6 in ES6 Essentials, and symbols in Classes, Symbols, Objects, and Decorators, we’re now in great shape to understand promises, iterators, and generators. Promises offer a different way of attacking asynchronous code flows. Iterators dictate how an object is iterated, producing the sequence of values that gets iterated over. Generators can be used to write code that looks sequential but works asynchronously, in the background, as we’ll learn toward the end of the chapter.
To kick off the chapter, we’ll start by discussing promises. Promises have existed in user-land for a long time, but they’re a native part of the language starting in ES6.
Promises
Promises can be vaguely defined as “a proxy for a value that will eventually become available.” While we can write synchronous code inside promises, promise-based code flows in a strictly asynchronous manner. Promises can make asynchronous flows easier to reason about—once you’ve mastered promises, that is.
Getting Started with Promises
As an example, let’s take a look at the new fetch
API for the browser. This API is a simplification of XMLHttpRequest
. It aims to be super simple to use for the most basic use cases: making a GET
request against an HTTP resource. It provides an extensive API that caters to advanced use cases, but that’s not our focus for now. In its most basic incarnation, you can make a GET /items
HTTP request using a piece of code like the following.
fetch
(
'/items'
)
The fetch('/items')
statement doesn’t seem all that exciting. It makes a “fire and forget” GET
request against /items
, meaning you ignore the response and whether the request succeeded. The fetch
method returns a Promise
. You can chain a callback using the .then
method on that promise, and that callback will be executed once the /items
resource finishes loading, receiving a response
object parameter.
fetch
(
'/items'
).
then
(
response
=>
{
// do something
})
The following bit of code displays the promise-based API with which fetch
is actually implemented in browsers. Calls to fetch
return a Promise
object. Much like with events, you can bind as many reactions as you’d like, using the .then
and .catch
methods.
const
p
=
fetch
(
'/items'
)
p
.
then
(
res
=>
{
// handle response
})
p
.
catch
(
err
=>
{
// handle error
})
Reactions passed to .then
can be used to handle the fulfillment of a promise, which is accompanied by a fulfillment value; and reactions passed to .catch
are executed with a rejection reason
that can be used when handling rejections. You can also register a reaction to rejections in the second argument passed to .then
. The previous piece of code could also be expressed as the following.
const
p
=
fetch
(
'/items'
)
p
.
then
(
res
=>
{
// handle response
},
err
=>
{
// handle error
}
)
Another alternative is to omit the fulfillment reaction in .then(fulfillment, rejection)
, this being similar to the omission of a rejection reaction when calling .then
. Using .then(null, rejection)
is equivalent to .catch(rejection)
, as shown in the following snippet of code.
const
p
=
fetch
(
'/items'
)
p
.
then
(
res
=>
{
// handle response
})
p
.
then
(
null
,
err
=>
{
// handle error
})
When it comes to promises, chaining is a major source of confusion. In an event-based API, chaining is made possible by having the .on
method attach the event listener and then returning the event emitter itself. Promises are different. The .then
and .catch
methods return a new promise every time. That’s important because chaining can have wildly different results depending on where you append a .then
or a .catch
call.
A promise is created by passing the Promise
constructor a resolver that decides how and when the promise is settled, by calling either a resolve
method that will settle the promise in fulfillment or a reject
method that’d settle the promise as a rejection. Until the promise is settled by calling either function, it’ll be in a pending state and any reactions attached to it won’t be executed. The following snippet of code creates a promise from scratch where we’ll wait for a second before randomly settling the promise with a fulfillment or rejection result.
new
Promise
(
function
(
resolve
,
reject
)
{
setTimeout
(
function
()
{
if
(
Math
.
random
()
>
0.5
)
{
resolve
(
'random success'
)
}
else
{
reject
(
new
Error
(
'random failure'
))
}
},
1000
)
})
Promises can also be created using Promise.resolve
and Promise.reject
. These methods create promises that will immediately settle with a fulfillment value and a rejection reason, respectively.
Promise
.
resolve
({
result
:
123
})
.
then
(
data
=>
console
.
log
(
data
.
result
))
// <- 123
When a p
promise is fulfilled, reactions registered with p.then
are executed. When a p
promise is rejected, reactions registered with p.catch
are executed. Those reactions can, in turn, result in three different situations depending on whether they return a value, a Promise
, a thenable, or throw
an error. Thenables are objects considered promise-like that can be cast into a Promise
using Promise.resolve
as observed in Creating a Promise from Scratch.
A reaction may return a value, which would cause the promise returned by .then
to become fulfilled with that value. In this sense, promises can be chained to transform the fulfillment value of the previous promise over and over, as shown in the following snippet of code.
Promise
.
resolve
(
2
)
.
then
(
x
=>
x
*
7
)
.
then
(
x
=>
x
-
3
)
.
then
(
x
=>
console
.
log
(
x
))
// <- 11
A reaction may return a promise. In contrast with the previous piece of code, the promise returned by the first .then
call in the following snippet will be blocked until the one returned by its reaction is fulfilled, which will take two seconds to settle because of the setTimeout
call.
Promise
.
resolve
(
2
)
.
then
(
x
=>
new
Promise
(
function
(
resolve
)
{
setTimeout
(()
=>
resolve
(
x
*
1000
),
x
*
1000
)
}))
.
then
(
x
=>
console
.
log
(
x
))
// <- 2000
A reaction may also throw
an error, which would cause the promise returned by .then
to become rejected and thus follow the .catch
branch, using said error as the rejection reason. The following example shows how we attach a fulfillment reaction to the fetch
operation. Once the fetch
is fulfilled the reaction will throw an error and cause the rejection reaction attached to the promise returned by .then
to be executed.
const
p
=
fetch
(
'/items'
)
.
then
(
res
=>
{
throw
new
Error
(
'unexpectedly'
)
})
.
catch
(
err
=>
console
.
error
(
err
))
Let’s take a step back and pace ourselves, walking over more examples in each particular use case.
Promise Continuation and Chaining
In the previous section we’ve established that you can chain any number of .then
calls, each returning its own new promise, but how exactly does this work? What is a good mental model of promises, and what happens when an error is raised?
When an error happens in a promise resolver, you can catch that error using p.catch
as shown next.
new
Promise
((
resolve
,
reject
)
=>
reject
(
new
Error
(
'oops'
)))
.
catch
(
err
=>
console
.
error
(
err
))
A promise will settle as a rejection when the resolver calls reject
, but also if an exception is thrown inside the resolver as well, as demonstrated by the next snippet.
new
Promise
((
resolve
,
reject
)
=>
{
throw
new
Error
(
'oops'
)
})
.
catch
(
err
=>
console
.
error
(
err
))
Errors that occur while executing a fulfillment or rejection reaction behave in the same way: they result in a promise being rejected, the one returned by the .then
or .catch
call that was passed the reaction where the error originated. It’s easier to explain this with code, such as the following piece.
Promise
.
resolve
(
2
)
.
then
(
x
=>
{
throw
new
Error
(
'failed'
)
})
.
catch
(
err
=>
console
.
error
(
err
))
It might be easier to decompose that series of chained method calls into variables, as shown next. The following piece of code might help you visualize the fact that, if you attached the .catch
reaction to p1
, you wouldn’t be able to catch the error originated in the .then
reaction. While p1
is fulfilled, p2
—a different promise than p1
, resulting from calling p1.then
—is rejected due to the error being thrown. That error could be caught, instead, if we attached the rejection reaction to p2
.
const
p1
=
Promise
.
resolve
(
2
)
const
p2
=
p1
.
then
(
x
=>
{
throw
new
Error
(
'failed'
)
})
const
p3
=
p2
.
catch
(
err
=>
console
.
error
(
err
))
Here is another situation where it might help you to think of promises as a tree-like data structure. In Figure 4-2 it becomes clear that, given the error originates in the p2
node, we couldn’t notice it by attaching a rejection reaction to p1
.
In order for the reaction to handle the rejection in p2
, we’d have to attach the reaction to p2
instead, as shown in Figure 4-3.
We’ve established that the promise you attach your reactions onto is important, as it determines what errors it can capture and what errors it cannot. It’s also worth noting that as long as an error remains uncaught in a promise chain, a rejection handler will be able to capture it. In the following example we’ve introduced an intermediary .then
call in between p2
, where the error originated, and p4
, where we attach the rejection reaction. When p2
settles with a rejection, p3
becomes settled with a rejection, as it depends on p2
directly. When p3
settles with a rejection, the rejection handler in p4
fires.
const
p1
=
Promise
.
resolve
(
2
)
const
p2
=
p1
.
then
(
x
=>
{
throw
new
Error
(
'failed'
)
})
const
p3
=
p2
.
then
(
x
=>
x
*
2
)
const
p4
=
p3
.
catch
(
err
=>
console
.
error
(
err
))
Typically, promises like p4
fulfill because the rejection handler in .catch
doesn’t raise any errors. That means a fulfillment handler attached with p4.then
would be executed afterwards. The following example shows how you could print a statement to the browser console by creating a p4
fulfillment handler that depends on p3
to settle successfully with fulfillment.
const
p1
=
Promise
.
resolve
(
2
)
const
p2
=
p1
.
then
(
x
=>
{
throw
new
Error
(
'failed'
)
})
const
p3
=
p2
.
catch
(
err
=>
console
.
error
(
err
))
const
p4
=
p3
.
then
(()
=>
console
.
log
(
'crisis averted'
))
Similarly, if an error occurred in the p3
rejection handler, we could capture that one as well using .catch
. The next piece of code shows how an exception being thrown in p3
could be captured using p3.catch
just like with any other errors arising in previous examples.
const
p1
=
Promise
.
resolve
(
2
)
const
p2
=
p1
.
then
(
x
=>
{
throw
new
Error
(
'failed'
)
})
const
p3
=
p2
.
catch
(
err
=>
{
throw
new
Error
(
'oops'
)
})
const
p4
=
p3
.
catch
(
err
=>
console
.
error
(
err
))
The following example prints err.message
once instead of twice. That’s because no errors happened in the first .catch
, so the rejection branch for that promise wasn’t executed.
fetch
(
'/items'
)
.
then
(
res
=>
res
.
a
.
prop
.
that
.
does
.
not
.
exist
)
.
catch
(
err
=>
console
.
error
(
err
.
message
))
.
catch
(
err
=>
console
.
error
(
err
.
message
))
// <- 'Cannot read property "prop" of undefined'
In contrast, the next snippet will print err.message
twice. It works by saving a reference to the promise returned by .then
, and then tacking two .catch
reactions onto it. The second .catch
in the previous example was capturing errors produced in the promise returned from the first .catch
, while in this case both rejection handlers branch off of p
.
const
p
=
fetch
(
'/items'
).
then
(
res
=>
res
.
a
.
prop
.
that
.
does
.
not
.
exist
)
p
.
catch
(
err
=>
console
.
error
(
err
.
message
))
p
.
catch
(
err
=>
console
.
error
(
err
.
message
))
// <- 'Cannot read property "prop" of undefined'
// <- 'Cannot read property "prop" of undefined'
We should observe, then, that promises can be chained arbitrarily. As we just saw, you can save a reference to any point in the promise chain and then append more promises on top of it. This is one of the fundamental points to understanding promises.
Let’s use the following snippet as a crutch to enumerate the sequence of events that arise from creating and chaining a few promises. Take a moment to inspect the following bit of code.
const
p1
=
fetch
(
'/items'
)
const
p2
=
p1
.
then
(
res
=>
res
.
a
.
prop
.
that
.
does
.
not
.
exist
)
const
p3
=
p2
.
catch
(
err
=>
{})
const
p4
=
p3
.
catch
(
err
=>
console
.
error
(
err
.
message
))
Here is an enumeration of what is going on as that piece of code is executed:
-
fetch
returns a brand newp1
promise. -
p1.then
returns a brand newp2
promise, which will react ifp1
is fulfilled. -
p2.catch
returns a brand newp3
promise, which will react ifp2
is rejected. -
p3.catch
returns a brand newp4
promise, which will react ifp3
is rejected. -
When
p1
is fulfilled, thep1.then
reaction is executed. -
Afterwards,
p2
is rejected because of an error in thep1.then
reaction. -
Since
p2
was rejected,p2.catch
reactions are executed, and thep2.then
branch is ignored. -
The
p3
promise fromp2.catch
is fulfilled, because it doesn’t produce an error or result in a rejected promise. -
Because
p3
was fulfilled, thep3.catch
is never followed. Thep3.then
branch would’ve been used instead.
You should think of promises as a tree structure. This bears repetition: you should think of promises as a tree structure.1 Let’s reinforce this concept with Figure 4-4.
It all starts with a single promise, which we’ll next learn how to construct. Then you add branches with .then
or .catch
. You can tack as many .then
or .catch
calls as you want onto each branch, creating new branches and so on.
Promise
chain.