Try to make all of the tests pass and win a prize: My unending admiration.
- NodeJS 7.8.0
git clone https://github.com/cttttt/js-control-flow-trials
cd js-control-flow-trials
npm install
npm test
#
# To fix errors in tests/##-something.js edit trials/##-something.js
#
Running synchronous code is easy peasey, as Javascript is executed from top to bottom (for the most part):
function (a,b,c) {
a();
b();
c();
}
To run synchronous code conditionally, use if
:
// for tips on truthy vs. falsey, ask Chris
function (value) {
if (value) {
// run iff value is truthy
} else {
// run iff value is falsey
}
}
To repeat a block of synchronous code, use a for
, do
, or while
loop:
function looper(list, doSomething) {
// a while loop:
var i=0;
while (i<list.length) {
doSomething(list[i]);
i++;
}
}
function looperFor(list, doSomething) {
// an equivalent for loop:
for (var i=0; i<list.length; i++) {
doSomething(list[i]);
}
}
function looperDo(list, doSomething) {
var i=0;
// runs doSomething() at least once (assumes non-empty array)
do {
doSomething(list[i]);
i++;
} while(i<list.length)
}
Some of the basic types in Javascript have methods that help make certain common flows easier. For example, Array
's have a method, .map()
. map
:
- Expects a
function
as an argument. - Runs this
function
on all of the elements in the targetArray
. - Returns a new
Array
containing the results of those transformations, without affecting the originalArray
.
function transformer(list, transform) {
return list.map(transform);
}
Sometimes operations take a long time. For instance, let's read a file:
function printAFile(name) {
return console.log("%s", require('fs').readFileSync(name));
}
While these operations run, no other Javascript can run because Node runs Javascript in a single thread.
If this function was run as part of a request handler for an http
request, or the code that sends a message through a websocket
, unrelated operations (sending unrelated responses, accepting unrelated connections) would suspended. This could make your server appear to be down.
One solution to this problem is to ask the Node runtime to perform slow operations and pass in a Javascript function that the runtime calls when the slow work is done:
function printAFile(name, callback) {
require('fs').readFile(function (err, data) {
console.log("%s", data);
});
}
This type of slow running function is called an error first callback function because the first argument of the callback is reserved for details on a possible error.
Error first callbacks put a lot of pressure on callers to ensure that errors are handled properly. If an error isn't handled, bad things could happen...like a request handler hanging indefinitely.
Another approach to running code asynchronously is to have the slow operation return a stand-in for its result, a Promise.
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printAFile(name) {
var apromise = fs.readFileAsync(name);
apromise.then(function (data) {
console.log("%s", data);
});
}
Promises are neat in that they allow you to use synchronous code to handle the results of asynchronous functions. They also allow you to defer error handling...or not handle it at all and have an operation immediately crash-but-not-hang (crashing is usually bad, but hanging is always bad).
Another cool benefit of Promises is that they can be chained because .then()
returns a Promise
. All promises represent a future result and what the one returned by .then()
is no exception: It represents the result (the return value) of the function that processed the Promise
. This result could be a plain old Javascript object, or a Promise
that represents some future value.
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printTwoFiles(name1, name2) {
var apromise = fs.readFileAsync(name1);
var anotherpromise = apromise.then(function (data) {
console.log("%s", data);
return fs.readFileSync(name2);
});
anotherpromise.then(function (data2) {
console.log("%s", data2);
});
}
Note here that the code that handled the first Promise
returned a Promise
representing a future value: The data read from file, name2
. .then()
returns a promise that represents this same value. As with all Promises, fetching this value involves using .then()
.
Note that this code actually seems more complicated, but it only looks that way beacuse we're storing all of the intermediary Promises
in variables. We don't have to. Here's an equivalent function:
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printTwoFiles(name1, name2) {
var apromise = fs.readFileAsync(name1)
.then(function (data) {
console.log("%s", data);
return fs.readFileSync(name2);
})
.then(function (data2) {
console.log("%s", data2);
});
}
This chaining is only possible because fs.readFileAsync()
actually returns a promise. To allow printTwoFiles
to be called as part of a chain, it should always return a Promise, even if no useful value is returned.
The time at when it returns is as important as the value, when it comes to controlling the flow of a program's execution:
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printTwoFiles(name1, name2) {
var apromise = fs.readFileAsync(name1)
.then(function (data) {
console.log("%s", data);
return fs.readFileSync(name2);
})
.then(function (data2) {
console.log("%s", data2);
});
return apromise;
}
That's about it on using Promise
s. All it involves is writing a bunch of synchronous code:
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printTwoFiles(name1, name2) {
var apromise = fs.readFileAsync(name1)
console.log("%s", data);
fs.readFileSync(name2);
console.log("%s", data2);
return apromise;
}
...and wrapping it up in all of this boilerplate:
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
function printTwoFiles(name1, name2) {
.then(function () {
}).then(function (data) {
})
.then(function (data2) {
});
}
In rare cases, you may need to create a Promise. An example is when you need to run a function that doesn't hand you a Promise
. Take, for example, setTimeout
.
To create a function that adds a delay, create a Promise
by running new Promise()
:
function waitsASecond() {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve('hello');
}, 1000)
})
}
// prints 'hello' after a 1s delay
waitASecond().then(function (message) {
console.log(message);
})
Here, the two functions resolve
and reject
passed into the callback provided to the Promise
constructor control when the Promise
resolves (i.e. when the callback to .then()
is called) and with what value (i.e. what value is provided to the callback given to then()
).
async
and await
are keywords that tell the Javascript compiler that you're working with Promises
, but would rather write code that looks like the easy to understand synchronous code.
async
is an annotation you can add to a function that tells the Javascript compiler that this function will call Promise
-returning-functions.
Behind the scenes, the Javascript compiler will instrument this function to make it easier for it to work with Promises
with far less boilerplate.
Oh and remember way back when I mentioned you should pay it forward and return a Promise
from any function that uses Promise
s? When you use the async
keyword, the compiler will handle this for you and wrap whatever the function returns in a Promise
:
async function return1 () {
return 1
}
// return1 actually returns a Promise that represents the value returned.
return1().then(function (one) {
console.log(one); // prints 1
})
await
is a special keyword that takes a Promise
and asks the Javascript runtime to (more or less) take this function off of the stack until after the Promise
is resolved. When execution resumes, the await
statement will evaluate to the future value represented by the Promise
.
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
async function printTwoFiles(name1, name2) {
var data1 = await fs.readFileAsync(name1);
console.log("%s", data1);
var data2 = await fs.readFileAsync(name2);
console.log("%s", data2);
}
...or simply...
var Promise = require('bluebird'),
fs = Promise.promisifyAll(require('fs'));
async function printTwoFiles(name1, name2) {
console.log("%s", await fs.readFileAsync(name1));
console.log("%s", await fs.readFileAsync(name2));
}