@rob_rich

JavaScript
Async Deep-dive

by Rob Richardson

@rob_rich

https://robrich.org/

About Me

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.

Agenda

Callbacks, Promises, async / await

  • How to use them
  • Pros & Cons
  • Many things in parallel
  • interop between them

Can I use this now?

I can use Promises now I can use async await now source: caniuse.com

Can I use this now?

I can use Promises and async await now source: https://node.green/

Why Async

Do things in parallel

MDI source: http://benhoff.net/qt-interface-design.html

Yield while I'm waiting

shared hosting source: https://itxdesign.com/vps-vs-shared-hosting/

Race Conditions

Race Conditions source: https://hacks.mozilla.org/2017/06/avoiding-race-conditions-in-sharedarraybuffers-with-atomics/

Node Event Loop

Race Conditions source: https://www.youtube.com/watch?v=S5XEU3XVMdo

Async Techniques

Callbacks

In the beginning ...

callLib(inp, args, function (err, res, ults) {
  if (err) {
    // handle error
    return;
  }
  // handle success
});

Callbacks

hard 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);
            });
        });
    });
});

Callbacks

easy 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);
    });
  });
});

Callbacks

hard Optional arguments

function mylib(arg, optionalArg, cb) {
  if (!cb && typeof optionalArg === 'function') {
    cb = optionalArg;
    optionalArg = undefined;
  }
  // now we can begin
}

Callbacks

hard 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

Callbacks

hard 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();
});

Callbacks

easy Pros

  • Simple scenarios
  • Args jump steps

hard Cons

  • Indent off the page
  • Libraries can't throw
  • Optional parameters
  • Parallel execution

Promises

Promises

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);
  });
}

Promises

easy "global" error handler

callLib1(inp, args, function (err) {
  if (err) {
    return handle(err);
  }
  done();
});
callLib1(inp, args)
  .then(done)
  .catch(handleError);

Promises

easy gracefully recover

callLib1(inp, args)
  .catch(err => ({err: err}))
  .then(callLib2)
  .then(done)
  .catch(handleError);

Promises

hard 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);

Promises

easy 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);

Promises

easy 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);

Promises

easy 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

Promises

easy 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

Promises

easy Pros

  • A handle during processing
  • Reduced nested wing
  • Global error handler
  • return and throw auto wrapped in promise
  • Parallel execution

hard Cons

  • Then-a-thon
  • Pass all results through all functions

Generators*

*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

easy Pros

  • Think sync, works async
  • throw works

hard Cons

  • Interop?

Interop: async & Promise

easy Promise 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);

Interop: async & Promise

easy await 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

easy 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

easy 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 sleep

function 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

easy Pros

  • Think sync, works async
  • throw works
  • Interop with Promises

hard Cons

Interop: Promise and Callback

easy 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

Interop: Promise and Callback

easy 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);
});

Interop: async and Callback

easy 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;
}

Interop: async and Callback

easy 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);
});

Interop: sync calls async

hard 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
});

The road ahead

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.

The road ahead

We'll upgrade our libraries from callbacks to async.

We'll throw and catch exceptions.

Consider Node's fs module.

JavaScript Async Deep-dive

evolve from callback to promise to async

Source: https://www.bram.us/2017/05/09/javascript-from-callbacks-to-promises-to-asyncawait-in-7-seconds/