Async Patterns

From Callbacks to Promises

Doing Async Stuff With Callbacks

We're going to set up a simple asynchronous scenario with setTimeout.

 

setTimeout accepts two arguments. The second argument is the amount of time to wait, and the first argument is a callback which will be evaluated when the time expires.

 

We're going to wrap setTimeout in a higher order function that will allow us to pass in an object to log, a success handler to evaluate in a happy case and an error handler to evaluate if something unexpected occurs.

//awesome utility library
const _ = require('lodash');

//simple logger
const logWaitObject = function (waitObject) {
  console.log(waitObject.order + ' ' + waitObject.length);
}

//simple error handler
const notNumberError = function (waitObject) {
  console.log('Not a number ' + waitObject.length);
}

//my fake asynchronous action
const doSomethingAsync = function (waitObject, handleAsyncSuccess, handleAsyncError) {
  if(!_.isNumber(waitObject.length)) {
    handleAsyncError(waitObject);
    return;
  }
  setTimeout(function ( ) {
    handleAsyncSuccess(waitObject);
  }, waitObject.length);
};

//callback for success
const handleAsyncSuccess = function (waitObject) {
  logWaitObject(waitObject);
};

//callback for error
const handleAsyncError = function (waitObject) {
  notNumberError(waitObject);
};

What Could Go Wrong?

const waitObjects = [
  { order: 'First', length: 900 },
  { order: 'Second', length: '**no duh, idiot**' },
  { order: 'Third', length: 300 },
  { order: 'Fourth', length: 100 },
];

doSomethingAsync(waitObjects[0], handleAsyncSuccess, handleAsyncError);
doSomethingAsync(waitObjects[1], handleAsyncSuccess, handleAsyncError);
doSomethingAsync(waitObjects[2], handleAsyncSuccess, handleAsyncError);
doSomethingAsync(waitObjects[3], handleAsyncSuccess, handleAsyncError);

The output order is unpredictable, something like:

Not a number **no duh, idiot**
Fourth 100
Third 300
First 900

So, chain the callbacks

doSomethingAsync(waitObjects[0],
  function (waitObject) {
    logWaitObject(waitObject);
    doSomethingAsync (waitObjects[1],
        function (waitObject) {
          logWaitObject(waitObject);
          doSomethingAsync (waitObjects[2],
            function (waitObject) {
              logWaitObject(waitObject);
              doSomethingAsync(waitObjects[3],
                function (waitObject) {
                  notNumberError(waitObject);
                },
                function (waitObject) {
                  notNumberError(waitObject);
                }
              );
            },
            function (waitObject) {
              notNumberError(waitObject);
            }
          );
        },
        function (waitObject) {
          notNumberError(waitObject);
        }
    );
  },
  function (waitObject) {
    notNumberError(waitObject);
  }
);

Welcome To The
Pyramid
Of

Doom

Doesn't Feel Great

Doing Async Stuff With Promises

We're going to set up a simple asynchronous scenario with setTimeout.

 

setTimeout accepts two arguments. The second argument is the amount of time to wait, and the first argument is a callback which will be evaluated when the time expires.

 

We're going to wrap setTimeout in a higher order function that will allow us to pass in an object and return a promise.

Promises

  • Proxy for a value not necessarily known
  • Has a pending, fulfilled or rejected state
  • Has a queued up handler to be executed when the promise is
    fulfilled or rejected
const _ = require('lodash');

//simple logger
const logWaitObject = function (waitObject) {
  console.log(waitObject.order + ' ' + waitObject.length);
}

//my fake asynchronous action
const doSomethingAsyncPromise = function (waitObject) {
  const timeoutPromise = new Promise(function (resolve, reject) {
    if (!_.isNumber(waitObject.length)) {
      reject(waitObject);
    }
    setTimeout(resolve, waitObject.length, waitObject);
  });
  return timeoutPromise;
}
const waitObjects = [
  { order: 'First', length: 900 },
  { order: 'Second', length: '**no duh, idiot**' },
  { order: 'Third', length: 300 },
  { order: 'Fourth', length: 100 },
];

const promiseArray = [];
doSomethingAsyncPromise(waitObjects[0])
.then(function (waitObject) {
  logWaitObject(waitObject);
  return waitObject;
})
.then(function (waitObject) {
  return doSomethingAsyncPromise(waitObject[1]);
})
.then(function (waitObject) {
  logWaitObject(waitObject);
})
.catch(function (waitObject) {
  console.log('not a number');
});

//Output Produces
First 900
not a number

Benefits

  • Easy to read Logical Flow
  • Error Handling in One Placef
  • Promise.all() handles an array of promises, resolves if they all resolve or rejects if any of them reject
  • Promise.race() similar to Promise.all() but resolves or rejects as soon as any promise it is handed resolves or rejects

Real Life Example!

We had an issue where we needed to do several api requests to get daily account balances and then perform an action once they were all completed.

loadDailyAccountBalances (accounts, startDate, endDate) {
    let dailyAccountBalances = [];
    let requestCount = accounts.length;

    accounts.forEach(account => {
      const requestUrl = ApiEndpoints.ACCOUNTS + '/' 
                            + account.guid 
                            + '/daily_account_balances' 
                            + '/from/' + startDate + '/to/' + endDate;

      request.get(requestUrl, {
        success (res) {
          requestCount -= 1;
          dailyAccountBalances = dailyAccountBalances.concat(res.daily_account_balances);

          if (requestCount <= 0) {
            ServerActions.dailyAccountBalancesLoaded(dailyAccountBalances);
          }
        }
      });
    });
  },
loadDailyAccountBalances (accounts, startDate, endDate) {
    let dailyAccountBalances = [];
    const requests = [];

    accounts.forEach(account => {
      requests.push({
        url: ApiEndpoints.ACCOUNTS + '/' + account.guid + '/daily_account_balances' + '/from/' + startDate + '/to/' + endDate
      });
    });

    const fetches = FetchUtils.createFetchArray(requests);

    Promise.all(fetches)
    .then(jsonArray => {
      jsonArray.forEach(json => {
        dailyAccountBalances = dailyAccountBalances.concat(json.daily_account_balances);
      });
      ServerActions.dailyAccountBalancesLoaded(dailyAccountBalances);
    })
    .catch(error) => {
      ServerActions.dailyAccountBalancesError(error.response.daily_account_balances, error);
    });
  }
Made with Slides.com