Asynchronous JavaScript
Overview
Cookbook approach to asynchronous code in modern JavaScript.
The JavaScript event loop and run-to-completion semantics.
Callbacks for event handlers.
Pyramid of doom.
Taming asynchronous code: promises.
Taming asynchronous code: async, await.
Cookbook Approach
Can use asynchronous code without understanding underlying concepts by using
"keywords" async and await:
If a function is documented as async or as returning a promise, then it is
possible to call it using the await keyword.
The await returns with the success value only when the underlying
asynchronous operation completes.
If an error occurs in the asynchronous function, then the resulting exception
can be handled using the usual try - catch .
The await keyword can only be used within functions declared using the
async keyword. Consequently, any use of asynchronous code within a
program will necessitate declaring the top-level function in the program
async .
Enables writing asynchronous code in a synchronous style.
Cookbook Approach to using fetch()
The browser provides fetch() to allow accessing resources asynchronously.
async function getUrl(url) {
const response = await fetch(url);
if (![Link]) {
throw new Error(`HTTP error '${[Link]}'`);
}
else {
return await [Link]();
}
}
This was used in the crawler discussed earlier.
Cookbook Approach to using MongoDB
Docs for connect(), db, collection, findOne() and close().
const mongo = require('mongodb').MongoClient;
async function(mongoUrl, dbName, collectionName, id) {
//bad code: reuse db conn; close() should be in finally.
try {
const client = await [Link](mongoUrl);
const db = [Link](dbName);
const collection = [Link](collectionName);
const value = await [Link]({_id: id});
await [Link]();
return value;
}
catch (err) {
[Link](err);
throw err;
}
}
The Need for Concurrency
Modern CPUs have clocks in the low GHz. That means individual CPU
operations occupy under 1 nanosecond.
Typically, I/O may take in the order of milliseconds which is around a million
times slower than CPU operations.
Highly inefficient to have CPU wait for an I/O operation to complete.
Need to concurrently do other stuff while waiting for I/O to complete.
Note that browser responsiveness is usually controlled by I/O responsiveness.
Approaches to Concurrency
Two commonly used approaches to concurrency:
Synchronous Blocking Model
When a program attempts to do I/O, the program blocks until the I/O is
completed. To allow concurrency, the operating system will schedule some
other activity while waiting for the I/O. The unit of scheduling is usually a
process or thread; leads to the process/thread model used by many current
OS's.
Asynchronous Event Model
When a program attempts to do I/O, it merely starts the I/O after registering
a callback handler to handle the I/O completion event. The program
continues running while the I/O is happening concurrently. The completion
of the I/O results in a event which results in the registered handler being
called.
Asynchronicity in JavaScript
JavaScript uses the asynchronous event model.
JavaScript started out simply using callbacks for asynchronous programming.
Promises introduced into JavaScript in ES6 around 2012.
Allow using asynchronous functions in a synchronous style using
async / await in ES2017.
Asynchronicity is required for message passing which is an essential
ingredient of Reactive Programming.
JavaScript Event Loop
The top-level JavaScript runtime consists of an event loop which pulls tasks
corresponding to completed events off a task queue and calls their handler function.
while ([Link]()) {
const task = [Link]();
const handler = [Link]();
[Link](); //pass suitable arguments
}
//terminate program
The [Link]() runs to completion.
Code does not need to deal with an event handler being interrupted.
Code still needs to deal with the fact that the order of running of event
handlers is not defined.
Run to Completion Consequences
In [Link]:
#!/usr/bin/env nodejs
//BAD CODE!!
function sleep(seconds) {
const stop = [Link]() + seconds*1000;
while ([Link]() < stop) {
//busy waiting: yuck!
}
}
setTimeout(() => [Link]('timeout'),
1000 /*delay in milliseconds*/);
sleep(5);
[Link]('sleep done');
Run to Completion Log
$ ./[Link]
sleep done
timeout
$
Because of run-to-completion semantics, it will always be the case that the sleep
done message will be output before the timeout message.
Why This Concurrency Model
JavaScript was designed as a language which should be easy for inexperienced
programmers to use for scripting dynamic behavior in browsers.
Browser reacts to user actions by generating events like key-press, mouse-
click, etc.
Browser programmer needs to provide optional event handlers for these events
in order to implement browser dynamic behavior.
Since every event handler runs to completion, programmer can simply
concentrate on code for that event, ignoring other events (at least for
independent events).
No need for the programmer to understand complex process / threading
models.
Lower overhead for I/O bound tasks; well suited for browser environment.
Playing with Asynchronous Functions
Simulate asynchronous success and failure using [Link] which uses
setTimeout() to run function asynchronously or fail after a 2-second delay:
const TIMEOUT_MILLIS = 2*1000;
export function asyncSucc(fn, ...args) {
setTimeout(fn, TIMEOUT_MILLIS, ...args);
}
export function asyncErr(msg) {
setTimeout(() => { throw new Error(msg) }, TIMEOUT_MILLIS);
}
// Utilities
export const p = [Link];
export function t() { return new Date().toTimeString(); }
Playing with Asynchronous Functions: Usage
> const { asyncSucc, asyncErr, p, t } =
await import('./[Link]')
undefined
> p(1, 2, 3)
1 2 3
undefined
> t()
'08:56:59 GMT-0400 (Eastern Daylight Time)'
> asyncSucc(() => p('done'))
undefined
> done
> asyncErr('some error message')
undefined
> Uncaught Error: some error message
>
Accessing Return Value of an Asynchronous
Operation
> function f() {
asyncSucc(() => { p('f run'); return 42; });
}
undefined
> let ret = f();
undefined
> f run
> ret
undefined
>
How do I get hold of the 42 return value.
Return Value of an Asynchronous Operation:
Another Attempt
Try using a global var to get hold of return value.
> function f() {
asyncSucc(() => { p('f run'); ret = 42; });
}
undefined
> ret = -1; f(); p(`ret after f() is ${ret}`);
ret after f() is -1
undefined
> f run
> ret
42
>
The only way to access the return value of an asynchronous operation is via a
parameter to the callback function.
> asyncSucc(callback => callback(42), v => [Link](v))
undefined
> 42
>
Placing Order Simulation
We use a simple async simulation of placing an order. Have an orderer object
which simulates consecutive asynchronous steps:
validate
Validate parameters.
order
Place order.
email
Send confirmation email.
The command-line allows up to two arguments:
1. The module to be used for placing an order.
2. An optional argument which specifies the step at which an error occurs.
Placing an Order using Callbacks: Happy Path
In [Link]
function doOrder(orderer, params) {
[Link](params, succ => {
[Link](succ, succ => {
[Link](succ, succ => {
[Link](succ);
});
});
});
}
$ ./[Link] [Link]
happy-cb-order validated; order placed; email sent
# same result, even tho placing order fails!!
# (extra "order" argument forces error when placing order)
$ ./[Link] [Link] order
happy-cb-order validated; order placed; email sent
$
Errors
//Normal exception catching
> try {
throw 'throwing';
} catch (ex) {
p(`caught ${ex}`);
}
caught throwing
undefined
Errors Continued
//Exception in Async not caught
try {
asyncErr('some error');
}
catch (ex) {
p(`caught ${ex}`);
}
undefined
> Uncaught Error: some error
>
Using Node [Link]() with Callback
for (const path of [ './[Link]',
'./[Link]', ]) {
[Link](path, (err, data) => {
if (err) {
[Link](`cannot read ${path}: ${err}`);
}
else {
[Link](`read ${path}:\n${[Link](0, 15)}...`);
}
});
}
Placing an Order using Callbacks with Error
Checking
In [Link]:
function doOrder(orderer, params) {
[Link](params, (succ, err) => {
if (err) {
[Link](err);
}
else { //validate ok
[Link](succ, (succ, err) => {
if (err) {
[Link](err);
}
else { //placeOrder ok
[Link](succ, (succ, err) => {
if (err) {
[Link](err);
}
else { //email ok
[Link](succ);
}
}); //email
} //placeOrder ok
}); //placeOrder
} //validate ok
}); //validate
}
Placing an Order using Callbacks with Error
Checking: Log
# happy path
$ ./[Link] [Link]
err-cb-order validated; order placed; email sent
# catches errors properly
$ ./[Link] [Link] validate
validate error
$ ./[Link] [Link] order
order error
$ ./[Link] [Link] email
email error
$
Problems with Callbacks
A top-level exception handler does not work for asynchronous callbacks since
the handler runs before the callback. Hence exceptions occurring within the
callback are not caught by the top-level exception handler.
If an asynchronous function result needs to be further processed by another
asynchronous function, then we need to have nested callbacks.
A chain of callbacks leads to the pyramid of doom because of nesting of
callbacks.
Promises
A Promise is an object representing the eventual completion or failure of an
asynchronous operation.
When a function which requires an asynchronous callback as an argument is
called, it returns immediately with an object called a pending Promise.
Subsequently, the callbacks can be added to the promise. The callbacks will be
called after the promise has been settled.
let promise = some_call_which_returns_promise(...);
promise.
then(callback1).
then(callback2).
...
catch(errorCallback);
Promise Advantages
Promises can be chained; this avoids the pyramid of doom.
Callbacks are never called before completion of current run of js event loop.
Callbacks added using then even after completion of the asynchronous
operation will still be called.
then() can be called multiple times on the same promise to add multiple
callbacks (called in order of insertion).
Allows catching errors much more easily using catch() ; similar to
exception handling.
then() can even be chained after a catch.
Promise API
new Promise(
/* executor */
function(resolve, reject) { ... }
);
Creates a promise.
resolve and reject are single argument functions.
Executor function executed immediately. Usually will start some kind of
asynchronous operation which may return some result.
1. If the async operation succeeds with some result succ, then the
executor function should call resolve(succ).
2. If the async operation fails with some error err, then the executor
function should call reject(err).
Outline of Using Promises for Asynch Operations
function doOperation(...params) {
return new Promise((resolve, reject) => {
asyncOperation(...params, (result) => { //callback
if (isOk(result)) {
resolve(result);
}
else {
reject(result);
}
});
});
}
doOperation(...).
then(result => { ... }).
catch( err => ...);
Promise States
Pending
The underlying operation is not yet complete.
Settled
The underlying operation completed; it is known whether or not it
succeeded resulting in two settled sub-states:
Fulfilled
The underlying operation completed successfully.
Rejected
The underlying operation failed.
A promise is settled only once. The state of the promise will not change once it is
settled.
Getting Promise Settlement: then()
[Link](value, err)
Arguments are one argument functions called when somePromise is settled;
specifically value / err are called with fulfillment / rejection value
depending on settlement.
Usually then() is called with only the value argument, with rejection of
somePromise handled using a catch() .
then() itself returns a promise; this allows chaining then 's.
If the function passed to then() returns a value, then the return'd
promise fulfills with that value.
If the function passed to then() throws an error, then the return'd
promise rejects with that error.
If the function passed to then() returns a promise, then the return'd
promise has the same settlement as it.
Getting Promise Rejection: catch()
[Link](err)
err is a one argument functions called with the rejection value of promise
somePromise.
catch() itself returns a promise; this allows continued promise chaining.
Return value is similar to that of then() .
nodejs [Link]()
API for nodejs evolved before promises were added to language.
[Link](fn) can be used with existing callback-based nodejs
library function fn to make it return a Promise .
For example [Link]([Link]) returns a wrapped version of
[Link] which will return a Promise .
Using Node [Link]() with Promise
function readFilePromise(path) {
return [Link]([Link])(path, 'utf8');
}
for (const path of [ './[Link]',
'./[Link]', ]) {
readFilePromise(path)
.then(data => {
[Link](`read ${path}:\n${[Link](0, 15)}...`);
})
.catch(err => {
[Link](`cannot read ${path}: ${err}`);
});
}
Playing with Promises
> pr = new Promise((resolve, reject) => resolve(22))
Promise { 22, ... }
> [Link]((v) => p(v))
Promise { <pending>, ... }
> 22
//Promise is settled only once
> pr = new Promise((succ) => { succ(42); succ(22); })
Promise { 42, ... }
> [Link]((v) => p(v))
Promise { <pending>, ... }
> 42
Playing with Promises: Chaining then()'s
> function f(a, b) { p(a); return a * b; }
undefined
> pr = new Promise((resolve) => resolve(22))
Promise { 22, ... }
> [Link]((val) => f(val, 2)).
then((val) => f(val, 3)).
then((val) => p(val))
> Promise { <pending>, ... }
> 22
44
132
>
Creating Settled Promises
[Link](value)
Returns a promise which is already fulfilled with value.
[Link](err)
Returns a promise which is already rejected with err.
Playing with Promises: Asynchronous Functions
> function f(a, b, ret) {
p(`${t()}: ${a}`);
setTimeout(() => ret(a*b), 2000);
}
undefined
> pr = [Link](22)
> pr.
then((v) => new Promise((succ) => f(v, 2, succ))).
then((v) => new Promise((succ) => f(v, 3, succ))).
then((v) => p(`${t()}: ${v}`))
> 08:21:42 GMT-0400 (Eastern Daylight Time): 22
08:21:44 GMT-0400 (Eastern Daylight Time): 44
08:21:46 GMT-0400 (Eastern Daylight Time): 132
>
Playing with Promises: Errors
> p(t()); pr1 =
[Link](new Error(t())); [Link](()=>{})
08:27:07 GMT-0400 (Eastern Daylight Time)
...
> p(t()); pr1.
then((v) => p(v)).
then((v) => p(v)).catch((err)=>p(err))
08:27:51 GMT-0400 (Eastern Daylight Time)
...
> Error: 08:27:07 GMT-0400 (Eastern Daylight Time)
...
Playing with Promises: Errors Continued
> pr1.
then((v) => p(`got value ${v}`)).
then((v) => p(`got value ${v}`)).
catch((e) => { p(`caught ${e}`); return 42; }).
then((v) => p(`got value ${v}`))
Promise { <pending>, ... }
> caught Error: 08:27:07 GMT-0400 (Eastern Daylight Time)
got value 42
>
Playing with Promises: Errors Continued
then()-chain continues past catch():
> [Link](1).
then((v) => { p(`then1: ${v}`); return v*2; }).
then((v) => { p(`then2: ${v}`); return v*2; }).
catch((e) => p(`caught ${e}`)).
then((v) => { p(`then3: ${v}`); return v*2; })
Promise { <pending>, ... }
> then1: 1
then2: 2
then3: 4
>
Placing an Order using Promises
In [Link]:
function doOrder(orderer, params) {
[Link](params)
.then(succ => [Link](succ))
.then(succ => [Link](succ))
.then(succ => [Link](succ))
.catch(err => [Link](err));
}
$ ./[Link] [Link]
promise-order validated; order placed; email sent
# catches errors properly
$ ./[Link] [Link] validate
validate error
$ ./[Link] [Link] order
order error
$ ./[Link] [Link] email
email error./[Link] [Link]
$
main program for asynchronous order simulator.
[Link]()
Given an iterable of promises, returns a promise containing array of fulfilled values,
or rejection if any promise rejected. (note that mulN(i) returns promise for N*i
after 2 second delay):
> [Link]([mul2(3), mul3(4), mul4(5)]).
then((v) => p(v))
Promise { <pending>, ... }
> [ 6, 12, 20 ]
// err() returns a rejected promise
> [Link]([mul2(3), err(3)(2), mul3(4), mul4(5)]).
then((v) => p(v)).
catch((e) => p(`caught ${e}`))
Promise { <pending>, ... }
> caught Error: err
[Link]() Continued
[Link]() runs all promises in parallel:
> p(t()); [Link]([mul2(3), mul3(4), mul4(5)]).
then((v) => p(`${t()}: ${v}`))
15:49:41 GMT-0500 (EST)
Promise { <pending>, ... }
> 15:49:43 GMT-0500 (EST): 6,12,20
Took 2 seconds to run all 3 functions even though each function takes 2 seconds
apiece.
[Link]()
Given an iterable of promises, returns a promise containing settlement of which
ever incoming promise completes first.
> [Link]([mul2(3), mul3(4), mul4(5)]).
then((v) => p(v))
Promise { <pending>, ... }
> 6
>
Placing an Order using Generators
Combine promises and generators to linearize control flow:
In [Link]:
function generatorOrder(orderer, params) {
const gen = doOrder(orderer, params);
[Link]().value //validation promise
.then(succ => [Link](succ).value) //order result promise
.then(succ => [Link](succ).value) //email result promise
.then(succ => [Link](succ)) //output overall result
.catch(err => [Link](err)); //catch any error
}
function* doOrder(orderer, params) {
const validationResult = yield [Link](params);
const orderResult =
yield [Link](validationResult);
yield [Link](orderResult);
}
Placing an Order using Generators: Log
# happy path
./[Link] [Link]
generator-order validated; order placed; email sent
# catches errors properly
$ ./[Link] [Link] validate
validate error
$ ./[Link] [Link] order
order error
$ ./[Link] [Link] email
email error
$
async / await
Extra syntax around promises and generators to allow writing asynchronous
code in a synchronous style.
If a function or function expression has the async (contextual) keyword in
front of it, then that function always returns a promise.
When the await (contextual) keyword is used in front of an expression
which is a promise, it blocks the program until the promise is settled. The
value of an await expression is the fulfillment value of the promise.
The await keyword can only be used within a async function.
Errors can be handled using try - catch .
Seems a big win.
Note that we may need to fall back on promises using [Link]() when
we want to run code in parallel rather than sequentially.
async / await Example
> function msgPromise() {
return new Promise(function (resolve) {
setTimeout(() =>
resolve(`hello@${t()}`), 2000)});
}
undefined
> async function msg(n) {
const m = await msgPromise();
return `${n}: ${m}`
}
undefined
Top-Level async / await Example: Invoking using
IIFE
Until ES-2022, top-level await not allowed. Used IIFE as workaround.
> ( async () => { //must use async to use await
p(await msg(22));
p(await msg(42));
})() //async IIFE
Promise { <pending>, ... }
> 22: hello@21:06:53 GMT-0500 (EST)
42: hello@21:06:55 GMT-0500 (EST)
>
Async sleep()
> async function sleep(millis) {
return new Promise((resolve) =>
setTimeout(() => resolve(), millis));
}
> p(t()); await sleep(2000); p(t()); //es-2022 top-level await
18:49:06 GMT-0400 (Eastern Daylight Time)
18:49:08 GMT-0400 (Eastern Daylight Time)
undefined
>
Placing an Order using async and await
In [Link]:
async function doOrder(orderer, params) {
try {
const validation = await [Link](params);
const order = await [Link](validation);
const result = await [Link](order);
[Link](result);
}
catch (err) {
[Link](err);
}
}
# happy path
./[Link] [Link]
async-await-order validated; order placed; email sent
# catches errors properly
$ ./[Link] [Link] validate
validate error
$ ./[Link] [Link] order
order error
$ ./[Link] [Link] email
email error
$