Asynchronous flow control
The material in this post is heavily inspired by Mixu's Node.js Book.
At its core, JavaScript is designed to be non-blocking on the "main" thread, this is where views are rendered. You can imagine the importance of this in the browser. When the main thread becomes blocked it results in the infamous "freezing" that end users dread, and no other events can be dispatched resulting in the loss of data acquisition, for example.
This creates some unique constraints that only a functional style of programming can cure. This is where callbacks come in to the picture.
However, callbacks can become challenging to handle in more complicated procedures. This often results in "callback hell" where multiple nested functions with callbacks make the code more challenging to read, debug, organize, etc.
async1(function (input: any
input, result1: any
result1) {
async2(function (result2: any
result2) {
async3(function (result3: any
result3) {
async4(function (result4: any
result4) {
async5(function (output: any
output) {
// do something with output
});
});
});
});
});
Of course, in real life there would most likely be additional lines of code to handle result1
, result2
, etc., thus, the length and complexity of this issue usually results in code that looks much more messy than the example above.
This is where functions come in to great use. More complex operations are made up of many functions:
- initiator style / input
- middleware
- terminator
The "initiator style / input" is the first function in the sequence. This function will accept the original input, if any, for the operation. The operation is an executable series of functions, and the original input will primarily be:
- variables in a global environment
- direct invocation with or without arguments
- values obtained by file system or network requests
Network requests can be incoming requests initiated by a foreign network, by another application on the same network, or by the app itself on the same or foreign network.
A middleware function will return another function, and a terminator function will invoke the callback. The following illustrates the flow to network or file system requests. Here the latency is 0 because all these values are available in memory.
function function final(someInput: any, callback: any): void
final(someInput: any
someInput, callback: any
callback) {
callback: any
callback(`${someInput: any
someInput} and terminated by executing callback `);
}
function function middleware(someInput: any, callback: any): void
middleware(someInput: any
someInput, callback: any
callback) {
return function final(someInput: any, callback: any): void
final(`${someInput: any
someInput} touched by middleware `, callback: any
callback);
}
function function initiate(): void
initiate() {
const const someInput: "hello this is a function "
someInput = 'hello this is a function ';
function middleware(someInput: any, callback: any): void
middleware(const someInput: "hello this is a function "
someInput, function (result: any
result) {
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(result: any
result);
// requires callback to `return` result
});
}
function initiate(): void
initiate();
State management
Functions may or may not be state dependent. State dependency arises when the input or other variable of a function relies on an outside function.
In this way there are two primary strategies for state management:
- passing in variables directly to a function, and
- acquiring a variable value from a cache, session, file, database, network, or other outside source.
Note, I did not mention global variable. Managing state with global variables is often a sloppy anti-pattern that makes it difficult or impossible to guarantee state. Global variables in complex programs should be avoided when possible.
Control flow
If an object is available in memory, iteration is possible, and there will not be a change to control flow:
function function getSong(): string
getSong() {
let let _song: string
_song = '';
let let i: number
i = 100;
for (let i: number
i; let i: number
i > 0; let i: number
i -= 1) {
let _song: string
_song += `${let i: number
i} beers on the wall, you take one down and pass it around, ${
let i: number
i - 1
} bottles of beer on the wall\n`;
if (let i: number
i === 1) {
let _song: string
_song += "Hey let's get some more beer";
}
}
return let _song: string
_song;
}
function function singSong(_song: any): void
singSong(_song: any
_song) {
if (!_song: any
_song) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("song is '' empty, FEED ME A SONG!");
}
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(_song: any
_song);
}
const const song: string
song = function getSong(): string
getSong();
// this will work
function singSong(_song: any): void
singSong(const song: string
song);
However, if the data exists outside of memory the iteration will no longer work:
function function getSong(): string
getSong() {
let let _song: string
_song = '';
let let i: number
i = 100;
for (let i: number
i; let i: number
i > 0; let i: number
i -= 1) {
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout)setTimeout(function () {
let _song: string
_song += `${let i: number
i} beers on the wall, you take one down and pass it around, ${
let i: number
i - 1
} bottles of beer on the wall\n`;
if (let i: number
i === 1) {
let _song: string
_song += "Hey let's get some more beer";
}
}, 0);
}
return let _song: string
_song;
}
function function singSong(_song: any): void
singSong(_song: any
_song) {
if (!_song: any
_song) {
throw new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error("song is '' empty, FEED ME A SONG!");
}
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(_song: any
_song);
}
const const song: string
song = function getSong(): string
getSong('beer');
// this will not work
function singSong(_song: any): void
singSong(const song: string
song);
// Uncaught Error: song is '' empty, FEED ME A SONG!
Why did this happen? setTimeout
instructs the CPU to store the instructions elsewhere on the bus, and instructs that the data is scheduled for pickup at a later time. Thousands of CPU cycles pass before the function hits again at the 0 millisecond mark, the CPU fetches the instructions from the bus and executes them. The only problem is that song ('') was returned thousands of cycles prior.
The same situation arises in dealing with file systems and network requests. The main thread simply cannot be blocked for an indeterminate period of time-- therefore, we use callbacks to schedule the execution of code in time in a controlled manner.
You will be able to perform almost all of your operations with the following 3 patterns:
- In series: functions will be executed in a strict sequential order, this one is most similar to
for
loops.
// operations defined elsewhere and ready to execute
const const operations: {
func: any;
args: any;
}[]
operations = [
{ func: any
func: function1, args: any
args: args1 },
{ func: any
func: function2, args: any
args: args2 },
{ func: any
func: function3, args: any
args: args3 },
];
function function executeFunctionWithArgs(operation: any, callback: any): void
executeFunctionWithArgs(operation: any
operation, callback: any
callback) {
// executes function
const { const args: any
args, const func: any
func } = operation: any
operation;
const func: any
func(const args: any
args, callback: any
callback);
}
function function serialProcedure(operation: any): void
serialProcedure(operation: any
operation) {
if (!operation: any
operation) {
process.exit(0); // finished
}
function executeFunctionWithArgs(operation: any, callback: any): void
executeFunctionWithArgs(operation: any
operation, function (result: any
result) {
// continue AFTER callback
function serialProcedure(operation: any): void
serialProcedure(const operations: {
func: any;
args: any;
}[]
operations.Array<{ func: any; args: any; }>.shift(): {
func: any;
args: any;
} | undefined
Removes the first element from an array and returns it.
If the array is empty, undefined is returned and the array is not modified.shift());
});
}
function serialProcedure(operation: any): void
serialProcedure(const operations: {
func: any;
args: any;
}[]
operations.Array<{ func: any; args: any; }>.shift(): {
func: any;
args: any;
} | undefined
Removes the first element from an array and returns it.
If the array is empty, undefined is returned and the array is not modified.shift());
- Limited in series: functions will be executed in a strict sequential order, but with a limit on the number of executions. Useful when you need to process a large list but with a cap on the number of items successfully processed.
let let successCount: number
successCount = 0;
function function final(): void
final() {
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(`dispatched ${let successCount: number
successCount} emails`);
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log('finished');
}
function function dispatch(recipient: any, callback: any): void
dispatch(recipient: any
recipient, callback: any
callback) {
// `sendMail` is a hypothetical SMTP client
sendMail(
{
subject: string
subject: 'Dinner tonight',
message: string
message: 'We have lots of cabbage on the plate. You coming?',
smtp: any
smtp: recipient: any
recipient.email,
},
callback: any
callback
);
}
function function sendOneMillionEmailsOnly(): void
sendOneMillionEmailsOnly() {
getListOfTenMillionGreatEmails(function (err: any
err, bigList: any
bigList) {
if (err: any
err) {
throw err: any
err;
}
function function (local function) serial(recipient: any): void
serial(recipient: any
recipient) {
if (!recipient: any
recipient || let successCount: number
successCount >= 1000000) {
return function final(): void
final();
}
function dispatch(recipient: any, callback: any): void
dispatch(recipient: any
recipient, function (_err: any
_err) {
if (!_err: any
_err) {
let successCount: number
successCount += 1;
}
function (local function) serial(recipient: any): void
serial(bigList: any
bigList.pop());
});
}
function (local function) serial(recipient: any): void
serial(bigList: any
bigList.pop());
});
}
function sendOneMillionEmailsOnly(): void
sendOneMillionEmailsOnly();
- Full parallel: when ordering is not an issue, such as emailing a list of 1,000,000 email recipients.
let let count: number
count = 0;
let let success: number
success = 0;
const const failed: any[]
failed = [];
const const recipients: {
name: string;
email: string;
}[]
recipients = [
{ name: string
name: 'Bart', email: string
email: 'bart@tld' },
{ name: string
name: 'Marge', email: string
email: 'marge@tld' },
{ name: string
name: 'Homer', email: string
email: 'homer@tld' },
{ name: string
name: 'Lisa', email: string
email: 'lisa@tld' },
{ name: string
name: 'Maggie', email: string
email: 'maggie@tld' },
];
function function dispatch(recipient: any, callback: any): void
dispatch(recipient: any
recipient, callback: any
callback) {
// `sendMail` is a hypothetical SMTP client
sendMail(
{
subject: string
subject: 'Dinner tonight',
message: string
message: 'We have lots of cabbage on the plate. You coming?',
smtp: any
smtp: recipient: any
recipient.email,
},
callback: any
callback
);
}
function function final(result: any): void
final(result: any
result) {
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(`Result: ${result: any
result.count} attempts \
& ${result: any
result.success} succeeded emails`);
if (result: any
result.failed.length) {
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(`Failed to send to: \
\n${result: any
result.failed.join('\n')}\n`);
}
}
const recipients: {
name: string;
email: string;
}[]
recipients.Array<{ name: string; email: string; }>.forEach(callbackfn: (value: {
name: string;
email: string;
}, index: number, array: {
name: string;
email: string;
}[]) => void, thisArg?: any): void
Performs the specified action for each element in an array.forEach(function (recipient: {
name: string;
email: string;
}
recipient) {
function dispatch(recipient: any, callback: any): void
dispatch(recipient: {
name: string;
email: string;
}
recipient, function (err: any
err) {
if (!err: any
err) {
let success: number
success += 1;
} else {
const failed: any[]
failed.Array<any>.push(...items: any[]): number
Appends new elements to the end of an array, and returns the new length of the array.push(recipient: {
name: string;
email: string;
}
recipient.name: string
name);
}
let count: number
count += 1;
if (let count: number
count === const recipients: {
name: string;
email: string;
}[]
recipients.Array<{ name: string; email: string; }>.length: number
Gets or sets the length of the array. This is a number one higher than the highest index in the array.length) {
function final(result: any): void
final({
count: number
count,
success: number
success,
failed: any[]
failed,
});
}
});
});
Each has its own use cases, benefits, and issues you can experiment and read about in more detail. Most importantly, remember to modularize your operations and use callbacks! If you feel any doubt, treat everything as if it were middleware!