by Rob Richardson
Rob Richardson is a software craftsman building web properties in ASP.NET and Node, Angular and React. He's a frequent speaker at conferences, user groups, and community events, and a diligent teacher and student of high quality software development. You can find this and other talks on https://robrich.org/presentations and follow him on twitter at @rob_rich.
Callbacks, Promises, async / await
Do things in parallelsource: http://benhoff.net/qt-interface-design.html |
Yield while I'm waitingsource: https://itxdesign.com/vps-vs-shared-hosting/ |
In the beginning ...
callLib(inp, args, function (err, res, ults) {
if (err) {
// handle error
return;
}
// handle success
});
The nested wing
callLib(req, function (err, res1) {
if (err) {
return handle(err);
}
callLib(function (err, res2) {
if (err) {
return handle(err);
}
callLib(function (err, res3) {
if (err) {
return handle(err);
}
callLib(function (err, res4) {
if (err) {
return handle(err);
}
done(res1, res2, res3, res4);
});
});
});
});
Args can jump steps
callLib1(req, function (err, res1) {
if (err) {
return handle(err);
}
callLib2(function (err, res2) {
if (err) {
return handle(err);
}
callLib3(res1, function (err, res3) {
if (err) {
return handle(err);
}
done(res2, res3);
});
});
});
Optional arguments
function mylib(arg, optionalArg, cb) {
if (!cb && typeof optionalArg === 'function') {
cb = optionalArg;
optionalArg = undefined;
}
// now we can begin
}
Can't throw
function mylib(arg, cb) {
if (!arg) {
throw new Error('arg is blank'); // <-- can't do this
}
// ...
}
mylib(arg, function (err, result) {
if (err) {
return handle(err);
}
});
// exception ends up here
Parallel execution
var inflight = 3; // we'll call 3 things
var results = [];
function done(err) {
if (err) {
return handle(err);
}
inflight--;
if (inflight > 0) {
return; // not done yet
}
success(results);
}
callLib1(req, function (err, res) {
if (err) {
return done(err);
}
results[0] = res;
done();
});
callLib2(req, function (err, res) {
if (err) {
return done(err);
}
results[1] = res;
done();
});
callLib3(req, function (err, res) {
if (err) {
return done(err);
}
results[2] = res;
done();
});
Pros
|
Cons
|
callLib1(inp, args)
.then(r => callLib2(r))
.then(done)
.catch(handleError);
function callLib1(inp, args) {
// all evergreen browsers, since Node 6
return new Promise(function (resolve, reject) {
// do stuff
if (err) {
return reject(err);
}
resolve(answer);
});
}
"global" error handler
callLib1(inp, args, function (err) {
if (err) {
return handle(err);
}
done();
});
callLib1(inp, args)
.then(done)
.catch(handleError);
gracefully recover
callLib1(inp, args)
.catch(err => ({err: err}))
.then(callLib2)
.then(done)
.catch(handleError);
Pass arguments through each function
callLib1(inp, args)
.then(r1 => callLib2(r1))
.then(r2 => callLib3(r2))
.then(r3 => callLib4(r1, r3)) // <-- broken here
.then(done)
.catch(handleError);
return
wraps in promise
callLib1(inp, args)
.then(function (r1) {
return new Promise((resolve, reject) => {
resolve(r1);
});
})
.then(function (r2) {
return r2;
})
.then(done)
.catch(handleError);
throw
rejects promise
callLib1(inp, args)
.then(function (r1) {
return new Promise((resolve, reject) => {
reject(new Error('go boom'));
});
})
.then(function (r2) {
throw new Error('go boom');
})
.then(done)
.catch(handleError);
A single result
var calls = 0;
function theLib() {
return new Promise(resolve => {
setTimeout(() => {
calls++;
console.log(`called ${calls} times`);
resolve();
}, 100);
});
}
console.log('calling theLib');
var p = theLib(inp, args);
console.log('called');
p.then(() => console.log('done1'));
setTimeout(() => {
p.then(() => console.log('done2'));
}, 200);
// output:
// calling theLib
// called
// called 1 times
// done1
setTimeout(() => {
p.then(() => {
console.log('done3');
p.then(() => console.log('done4'));
});
}, 300);
// output:
// done3
// done4
setTimeout(() => {
console.log('calling theLib');
theLib()
.then(() => console.log('done5'));
}, 400);
// output:
// calling theLib
// called 2 times
// done5
Parallel execution
let p1 = callLib1(inp, args);
let p2 = callLib2(inp, args);
let p3 = callLib3(inp, args);
Promise.all([p1, p2, p3])
.then(done)
.catch(handleError);
Stops at first failure or after all succeed
Pros
|
Cons
|
*Generators are hard ... just don't
function* idMaker() {
var index = 0;
while (true) {
yield index++;
}
}
var gen = idMaker(); // "Generator { }"
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator
async
& await
async
& await
Clearly stolen inspired by C#
Identical syntax to C#
Under the hood, it's promises and generators
Think: resumable state machine
async
& await
async function myFunc() {
try {
// all evergreen browsers, since Node 7.6
let p1 = await callLib1(inp, args);
let p2 = await callLib2(inp, args);
let p3 = await callLib3(inp, args);
done(p1, p2, p3);
} catch (err) {
handleError(err);
}
}
async
& await
async function lib(inp, arg) {
if (!inp) {
throw new Error('inp is blank');
}
let res = await longRunningTask();
return res;
}
async
& await
Pros
|
Cons
|
async
& PromisePromise calls async fn
async function callLib(inp, arg) {
if (!inp) {
throw new Error('inp is blank');
}
let res = await longRunningTask();
return res;
}
callLib(inp, arg)
.then(res => done(res))
.catch(errorHandler);
async
& Promiseawait a Promise
function callLib(inp, arg) {
return new Promise((resolve, reject) => {
resolve(true);
});
}
async function () {
try {
let res = await callLib(inp, arg);
return res;
} catch (err) {
handleError(err);
}
}
async
& await
Parallel execution
async function callLib1() { /* ... */ }
async function callLib2() { /* ... */ }
async function () {
let p1 = callLib1(); // <-- don't await them here
let p2 = callLib2();
let results = await Promise.all([p1, p2]); // <-- await it here
return results;
}
async
& await
Parallel execution
async function callLib1() { /* ... */ }
async function callLib2() { /* ... */ }
async function () {
let p1 = callLib1(); // <-- don't await them here
let p2 = callLib2();
// await each of them after calling all of them
return {
r1: await p1,
r2: await p2
};
}
async
sleepfunction sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function () {
try {
let stuff = callLib1();
await sleep(100);
let res = callLib2();
return res;
} catch (err) {
handleError(err);
}
}
async
& await
Pros
|
Cons |
Promise calls callback
function callbackFunction(inp, args, cb) {
// ...
cb(null, res);
}
// since Node 8.0
const promisify = require('util').promisify;
const callbackAsPromise = promisify(callbackFunction);
callbackAsPromise(inp, args)
.then(done)
.catch(errorHandler);
Promisify polyfill: https://gist.github.com/nijikokun/31f9423bd6becf10612a43e74437c5f9
Callback calls promise
function promiseFunction(inp, args) {
return new Promise((resolve, reject) => {
// ...
resolve(res);
});
}
// since Node 8.2
const callbackify = require('util').callbackify;
const promiseAsCallback = callbackify(promiseFunction);
promiseAsCallback(inp, args, function (err, res) {
if (err) {
return handleError(err);
}
done(res);
});
Async calls callback
function callbackFunction(inp, args, cb) {
// ...
cb(null, res);
}
// since Node 8.0
const promisify = require('util').promisify;
const callbackAsPromise = promisify(callbackFunction);
async function () {
// ...
let res = await callbackAsPromise(inp, args);
// ...
return res;
}
Callback calls async
async function asyncFn(inp, args) {
await longRunningTask();
return res;
}
// since Node 8.2
const callbackify = require('util').callbackify;
const asyncAsCallback = callbackify(asyncFn);
asyncAsCallback(inp, args, function (err, res) {
if (err) {
return handleError(err);
}
done(res);
});
sync calls async
function libFn(inp, args, cb) {
setTimeout(function () {
cb(null, {res:'ult'});
}, 1);
}
function syncFunction () {
var result = null;
libFn(inp, args, function (err, res) {
result = res;
});
// callback hasn't run yet
return result; // <-- result is still null
});
callback, promise, async:
None of these allow you to cancel.
Rather you just stop waiting
and ignore the results when it finishes.
Use bluebird or decide not to.
We'll upgrade our libraries from callbacks to async.
We'll throw and catch exceptions.
Consider Node's fs module.
Source: https://www.bram.us/2017/05/09/javascript-from-callbacks-to-promises-to-asyncawait-in-7-seconds/