Hello, my name is Seth Bergman. I am a

Full Stack Engineer

focused on helping companies scale. I love learning about software architecture, containers, open source programming and automation. I use technologies that drive innovation, speed up development and provide continuous delivery of awesome software.

ES6 Learning Notes - Asynchronous Development

Callbacks

The most basic form of async programming in JS is the callback. The caller spawns an asynchronous operation. When the caller initiates this process, it passes the process to a callback, trusting that when the process finishes, it will invoke the callback.

When the async process finishes, the callback is put onto the call stack, and will execute when all the other processes ahead of it are complete.

There are some problems:

  • Only the caller can be notified that the async process has completed.
  • No other interested parties can act, unless the callback informs them.
  • Error handling is difficult.
  • Handling multiple callback processes at once is hard.
  • The callback is responsible for multiple things:
    • Processing the async call
    • Starting other processes that want to execute

Callback Example:

function getCompanyFromOrderId(orderId) {  
  getOrder(orderId, function(order) {
    getUser(order.userId, function(user) {
      getCompany(user.companyId, function(company) {
        // do something with company
      });
    });
  });
}

In this example, first we get the order, then when that's complete we get the user, then when that's complete we can get the company.

Notice that each callback has to process data by calling the next function, but also pass the appropriate callback into the next function.

When we add in exception handling, it's even worse:

function getCompanyFromOrderId(orderId) {  
  try {
    getOrder(orderId, function(order) {
      try {
        getUser(order.userId, function(user){
          try {
            getCompany(user.companyId, function(company) {
              try {
                 // do something with company
              } catch(ex) {
                // handle exception
              }
            });
          } catch(ex) {
            // handle exception
          }
        });
      } catch(ex) {
        // handle exception
      }
    });
  } catch(ex) {
    // handle exception
  }
}

So what's the solution?

Promises

Promises are what save us from "Callback Hell". A promise is an object which represents a handle to listen to the results of an async operation, whether it succeeds or fails. The promise "promises" to alert you when the async operation is done, and give you the results of that operation.

Promises are composable, which means you can take two promises that represent two different async operations, and chain them together so one happens after the other, or wait for them both to run and do a new operation when both are complete. You can make other promises that depend on the results of a previous promise, and succeeds when it succeeds, or fails when it fails.

A promise is made up of two parts:

The Control of a Promise

  • Also called "the deferred".
  • May be a separate object, or a callback.
  • Gives the creator of the promise the ability to mark the promise as "succeeded" or "failed".

The Promise itself

  • Object can be passed around
  • Enables interested parties to register actions to take when the async operation completes.
  • Flow control no longer responsibility of the handler
  • Error handling is much easier

 

A promise exists in one of three states:
1. Pending (Not yet completed)
2. Fulfilled (Completed successfully)
3. Rejected (Failed)

Note: the "rejected" state means we are no longer dealing with exception handling. We need to let other functions know that the promise failed so they have to do something.

Same example as the Callback + Error Handling, but with Promises:

function getCompanyFromOrderId(orderId){  
  getOrder(orderId).then(function(order) {
    return getUser(order.userId);
  }).then(function(user) {
    return getCompany(user.companyId);
  }).then(function(company) {
    // do something with company
  }).then(undefined, function(error) {
    // handle error
  })
}

Promise Basics

Note: This code is originally used in Jasmine tests

Resolve:

var promise = new Promise(function(resolve, reject){  
  resolve(1);
});

promise.then(function(data) {  
  console.log(data); // 1
  done();
});

Reject:

var promise = new Promise(function(resolve, reject){  
  reject(Error('I am Error'));
});

promise.then(function() {  
  // success
}, function(error) {
  console.log(error.message); // 'I am Error'
});

There is a shorthand if all you care about is a failure:

var promise = new Promise(function(resolve, reject){  
  reject(Error('I am Error'));
});

promise.catch(function(error) {  
  console.log(error.message); // 'I am Error'
});

 

Resolving a Promise with Another Promise

var previousPromise = new Promise(function(resolve, reject){  
  resolve(3);
});

var promise = new Promise(function(resolve, reject) {  
  resolve( previousPromise );
});

promise  
  .then( function(data) {
    console.log(data); // 3
  })

Advanced Promises

Chaining promises to make multiple async calls sequentially:

function getOrder(orderId) {  
  return Promise.resolve({userId: 35});
}

function getUser(userId) {  
  return Promise.resolve({companyId: 18});
}

function getCompany(companyId) {  
  return Promise.resolve({name: 'Pluralsight'})
}


getOrder(3).then(function(order) {  
  return getUser(order.userId);
}).then(function(user) {
  return getCompany(user.companyId);
}).then(function(company) {
  company.name; // 'Pluralsight'
}).catch(function(error) {
  // handle error
});

Execute after all promises have been returned

function getCourse(courseId) {  
  var courses {
    1: {name: 'Intro to Cobol'},
    2: {name: 'Another class'},
    3: {name: 'Class 3'}
  }

  return Promise.resolve(courses[courseId]);
}


var courseIds [1, 2, 3];  
var promises = [];

for(var i = 0; i < courseIds.length; i++){  
  promises.push(getCourse(courseIds[i]));
}

// 
Promise.all(promises).then(function(values) {  
  values.length; // 3
})

How to make a promise resolve after the very first of a set of promises resolves:

If you have several async processes going on and you want something to happen after the first one resolves, then you would use the race function.

var courseIds [1, 2, 3];  
var promises = [];

for(var i = 0; i < courseIds.length; i++){  
  promises.push(getCourse(courseIds[i]));
}

Promise.race(promises).then(function(firstValue) {  
  firstValue.name; // should be defined, but we don't know what order the promises resolve in.
})

Basic Asynchronous Generators

We can use generators to make our async code more readable.

Say we want to log 3 strings to the console, and between each log we want to pause for half a second.

Without Generators

function oldPause(delay, cb) {  
  setTimeout(function() {
    console.log('paused for ' + delay + 'ms');
    cb();
  }, delay);
}

console.log('start');  
oldPause(500, function() {  
  console.log('middle');
  oldPause(500, function() {
    console.log('end');
  });
});

With Generators

// IIFE to create variables without polluting the global namespace
(function() {
  var sequence;

  var run = function(generator) {
    sequence = generator();
    var next = sequence.next();
  }

  // Notify that the pause function has completed
  var resume = function() {
    sequence.next();
  }

  // Put it all inside an object that can be accessed anywhere
  window.async = {
    run: run,
    resume: resume
  }
}());

function pause(delay) {  
  setTimeout(function() {
    console.log('paused for ' + delay + 'ms');
    async.resume();
  }, delay);
}

function* main() {  
  console.log('start');
  yield pause(500);
  console.log('middle');
  yield pause(500);
  console.log('end');
}

async.run(main);

More Async Generators

Imagine we are writing a program that will conduct stock trades.

// IIFE to create variables without polluting the global namespace
(function() {
  var sequence;

  var run = function(generator) {
    sequence = generator();
    var next = sequence.next();
  }

  // Notify that the pause function has completed
  // Any caller to
  var resume = function(value) {
    sequence.next(value);
  }

  // Put it all inside an object that can be accessed anywhere
  window.async = {
    run: run,
    resume: resume
  }
}());

function getStockPrice() {  
  // $.get('/prices', function(prices) {
  //   mimic jQuery's get request
  // }); 

  setTimeout(function() {
    async.resume(50);
  }, 300);
}

function executeTrade() {  
  setTimeout(function() {
    console.log('trade completed');
    async.resume();
  }, 300);
}

function* main() {  
  // when this function calls sequence.next, 
  // we're going to pass in the result of the yield statement 
  // which will be the price variable.
  var price = yield getStockPrice(); 

  if(price > 45) {
    yield executeTrade();
  } else {
    console.log('trade not made');
  }
}

async.run(main);  

Asynchronous Generators and Promises

Let's pair up our async generators with promises.

One of the downsides to the async generators we've been working with is that they must be aware that they're being called from within an async generator, so they have to be able to call async.resume() or async.fail().

(function() {
  var run = function(generator) {
    var sequence;
    var process = function(result) {
      // parameter receives the value that the promise resolves with
      result.value.then(function(value) {
        if(!result.done) {
          process(sequence.next(value))
        }
      }, function(error) {
        if(!result.done) {
          process(sequence.throw(error));
        }
      })
    } 

    sequence = generator();
    var next = sequence.next();
    process(next);
  }


// Because we are going to use promises,
// the promises themselves will be the notifications to `asyncP`
// as to whether they are done or failed.
  window.asyncP = {
    run: run,
  }
}());

function getStockPriceP() {  
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      resolve(50);
    }, 300);
  });
}

function executeTradeP() {  
  return new Promise(function(resolve, reject){
    setTimeout(function() {
      resolve();
    }, 300);
  });
}

function* main() {  
  try {
    var price = yield getStockPriceP();
    if(price > 45) {
      yield executeTradeP();
    } else {
      console.log('trade not made');
    }
  } catch(ex) {
    console.log('error! ' + ex.message);
  }
  done();
}

asyncP.run(main);  

Generators and promises take care of asynchronous operations and error handling, while keeping your code clean and easy to follow.