Javascript Async Patterns

October 2016

Follow along at:

http://itsnull.com/

http://itsnull.com/presentations/js-async/

Created by Kip Streithorst / @itsnull

Overview

  • Sync vs. Async
  • Fixes for async
  • Demos
  • Gotchas
  • Looking forward

Our API

  • getNearbyStores(zipCode) -> list of ids
  • getStoreDetails(storeId) -> details of store
  • Both functions make call to server

Using our API - Ideally

  • How we'd like to use:
    function getClosestStore(zipCode) {
      var storeIds = getNearbyStores(zipCode);
      if (storeIds.length) {
        return getStoreDetails(storeIds[0]);
      }
      return null;
    }
                  

Why can't I do that in browser?

  • Each browser tab has a single UI thread
  • That thread is shared between browser and your code
  • So, if getNearbyStores took 5 seconds, UI thread would be blocked entire time
  • So, browser vendors never added synchronous apis, only async apis
  • Pass a callback to browser, when IO is complete, browser will call your callback

Using our API - Reality

  • Real example, using AJAX which is async:
    function getClosestStore(zipCode, successCallback, failureCallback) {
      getNearbyStores(zipCode, function (storeIds) {
        if (storeIds.length) {
          getStoreDetails(storeIds[0], function (storeDetails) {
            successCallback(storeDetails);
          }, failureCallback);
        } else {
          successCallback(null);
        }    
      }, failureCallback);
    }
                  

Callbacks - PROS

  • Long history of usage
  • Especially for events: click, keypress
  • Well understood, simple for browsers to implement.

Callbacks - CONS

  • Inverts the function weirdly:
                  
                  var retData = performOperation();
                  //... code that comes after
                  //becomes
                  performOperation(function (retData) {
                    //.. code that comes after
                  })
                  
  • Are unweidly when chained, wasn't a problem for event handlers, but now every IO operation causes another level of nesting.
  • Much harder to read the control flow

Thought experiment time

  • Let's work through some thought experiments
  • Goal is to improve flow and readability
  • We will "invent" our own concepts
  • Similiarity to any real or existing concepts is just that for now

Use a smart object

  • What if we returned something that told us when the operation was complete?
  • Let's call it Future
  • Has 3 states: Start, Resolved, Rejected
  • Has .resolve, .reject to move object into one of the two final states
  • Has a .done function - done(successCallback)
  • Has a .fail function - fail(failureCallback)
  • Now, our function returns something instead of taking an callback argument

Future - state flow

getClosestStore - Future

  • Implement with future:
    function getClosestStore(zipCode) {
      var retValue = new Future();
      getNearbyStores(zipCode).done(function (storeIds) {
        if (storeIds.length) {
          getStoreDetails(storeIds[0]).done(function (storeDetails) {
            retValue.resolve(storeDetails);
          }).fail(retValue.reject);
        } else {
          retValue.resolve(null);
        }    
      }).fail(retValue.reject);
      return retValue;
    }
                  

So?

  • PRO: Functions now return values
  • PRO: Functions are only passed their arguments, not extra callbacks
  • CON: Had to manually create our return value, e.g. Future
  • CON: Had to manually resolve, reject it
  • Did you notice? If something else rejects, we will likely reject
  • If something else resolves, we will likely resolve as well

Add chaining

  • Let's code that default logic into a new function
  • .then(successCallback, failureCallback)
  • The .then always returns a new Future, call it X
  • If either callback returns a value, X resolves with that value
  • If either callback has an exception, X rejects with that exception

getClosestStore - Future w/ Chaining

  • Implement with future w/ chaining:
    function getClosestStore(zipCode) {  
      return getNearbyStores(zipCode).then(function (storeIds) {
        if (storeIds.length) {
          return getStoreDetails(storeIds[0]);
        }
        return null;
      });
    }
                  

Good enough?

  • Pretty close to our ideal
  • No direct references to future, just the .then method
  • Functions always return values, not passed callbacks
  • One inconvenience, have to put code that uses the result into a callback

Can language help?

  • Maybe, the language could help?
  • C# has async/await keywords, might those help?
    var initialData = await getInitialData();
    var result = await parseData(initialData);
    printResult(result);
    
  • The code that needs initialData result is put into a callback automatically by language, that's what await keyword does.

getClosestStore - async/await

  • Implement with async/await:
    
    function async getClosestStore(zipCode) {  
      var storeList = await getNearbyStores(zipCode);
      if (storeList.length) {
        return await getStoreDetails(storeList[0]);
      }
      return null;
    }
                  

Full circle

  • Our ideal
    
    function getClosestStore(zipCode) {  
      var storeList = getNearbyStores(zipCode);
      if (storeList.length) {
        return getStoreDetails(storeList[0]);
      }
      return null;
    }
                  
  • What we have now
    
    function async getClosestStore(zipCode) {  
      var storeList = await getNearbyStores(zipCode);
      if (storeList.length) {
        return await getStoreDetails(storeList[0]);
      }
      return null;
    }
                  

Nice, now what?

  • We just covered the history of solutions
  • Demo async/await running in browsers
  • Talk gotchas

Promise philosophy

  • Functions either: return a single value or throw an exception, e.g. Error subclass
  • Promises either: resolve with a single value or reject with an exception, e.g. Error subclass
  • So, when an async function returns a promise it is effectively doing the same thing a synchronous function does
  • This is the appropriate mental model
  • Also what async/await syntax depends upon

Exceptions and Promises

  • getClosestStore(12121).then(function (storeDetails) {
      if (store.open) { //has an exception?         
        //do stuff
      }
    });
  • .then returns a Promise, since callback has an exception, returned Promise is rejected
  • I don't have an additional .then or a .catch
  • Our mental model says that is an unhandled exception

Async/Await Demo

  • Chrome v55 or later supports async/await already
  • MS Edge supports behind experimental flag, since Sept. 2015
  • Or transpile w/ BabelJS and add some polyfills
  • Or use Typescript 2.1 (released December 2016)
  • Code for demos on GitHub

Spec History

  • Initial Promise/A spec (2010)
  • JQuery team implemented incorrectly (2011)
  • New Promise/A+ spec (2012), only describes .then method
  • Javascript Language - ES 2015 (e.g ES 6) adds Promises/A+
  • Javascript Language documents constructor, .catch method
  • Promise documentation on MDN
  • Async/Await scheduled for next version of JS language ES 2017

How to use AJAX

  • Use fetch provided by browser
  • Use your framework's method, must return Promises/A+ compatible
  • Angular 1.x, $http
  • Angular 2.x, http service but then need to call .toPromise()
  • Don't use JQuery $.ajax, NOT Promises/A+ compatible

Fetch

  • New cross browser standard
  • Simple function, similiar to $.ajax, $http.get, returns Promise/A+
  • fetch('/api/users').then(function (response) {
      //processing
    });
    fetch('/api/users', {method: 'POST', body: new FormData(form)});
    
  • Supported by latest browsers: Edge, Firefox, Chrome, Opera, Android
  • Polyfill for older browsers, may also require a Promise polyfill as well

Warning - Be wary of JQuery

  • $.Deferred has a .then method, doesn't implement Promises/A+
  • $.ajax returns $.Deferred
  • Not interoperable with async/await or other libraries
  • Finally fixed in JQuery 3.0 (June 2016)

Warning - No more cancellation

  • Promises don't support cancellation
  • Cancellation is highly useful when dealing with multiple in-flight requests where UI only cares about latest response
  • Have a search page, user starts search, changes search, starts another search
  • Search results make come back out-of-order
  • Fix: Pass ordering key to server, it passes it back to client, ignore old responses
  • Fix: Cancel prior unfinished request before starting a new request

Warning - No more cancellation, cont.

  • XMLHttpRequest supports cancellation
  • $.ajax supports cancellation, but not Promises/A+
  • Promises don't support cancellation
  • Fetch doesn't support cancellation
  • $.ajax in JQuery 3.0 supports cancellation and Promises/A+
  • Browser vendors are brainstorming possible solutions

Warning - Uncaught rejections

  • Remember exception in callback turns into rejection from Promise
  • Need a .then or .catch to handle
  • If you don't, it's an uncaught rejection
  • All browsers (except Chrome), silently fail, no message or ability to find it
  • Makes it much harder to find bugs
  • Bluebird.js provides a solution

Bluebird.js features

  • Will report errors to console for uncaught rejections in all browsers, even in Chrome
  • Polyfills Promises for older browsers
  • Encourages proper promise usage, e.g. reports error if rejecting with something other than an exception
  • Debugging across an async jump can be difficult, latest dev tools in browsers fix this
  • Bluebird provides long stack traces in older browsers that don't have latest dev tool features
  • Provides a number of additional helpful methods

Promises

Beyond Promises

  • Promises handle a specific use case for async
  • var result = doSomething();
  • Great for AJAX, File I/O and delays, e.g. wait 5 seconds
  • But browsers have two other async operations
  • Events: click, keypress, mousemove
  • Timers: fire every 10 seconds

Beyond Promises, cont.

  • A click can happen 0-n times
  • It's kind of like a list, or maybe a infinite range, or wait a stream of events
  • For C# devs, IEnumerable<ClickEvent>??
  • But it's push not pull, e.g. the user pushes the event to us, we can't demand the next click
  • There is a pattern for this, Observable, e.g. subscribe
  • What if we treat streams as first-class objects and provide functions to filter them, combine then, e.g. same as we do lists

Beyond Promises, cont.

  • There is a Stage 1 Draft for next version of JS language
  • RxJs is a javascript library that implements parts of this proposal
  • Angular 2.x returns RxJs Observable from http service, can still get promise using .toPromise method
  • Spec is still early, may not make it into JS. Browsers may not support
  • Only one implementation currently
  • Promises, async/await fix most common issue: nested callbacks

Thanks, Any Questions?