JavaScript Promises:

@adamterlson

adam.terlson@gmail.com

Now even awesomer

In the beginning

function entireWorkflow(callback) {
    fs.readdir('/some/path', function (err, fileNames) {
        // Often forgotten or ignored is handling err
        if (err) throw new Error("We're screwed");
        
        fileNames.forEach(function (name) {
            fs.readFile(name, function (fileErr, file) {
                // Especially when doing nested or more complex flows
                if (fileErr) throw new Error("This is getting seriously annoying");
                
                // Insert value-adding code here
            });
        });
        
        // Uhh... when do I call `callback()`?
    });
}

Promises 101

  • Represent a future value
  • Producer resolves with a value or rejects with an exception
  • Consumers listen for success with .then and exceptions with .catch
  • State is initially "Pending" and becomes "Resolved" or "Rejected"
  • Once state is set, it cannot change

Making Promises in <= ES5

  • Use a library (jQuery, Q, Bluebird, etc)
  • Commonly use a deferred
function asyncStuff() {
    var dfd = Q.defer();
    
    dfd.resolve(10);
    // or
    dfd.reject(new Error('Reason'));
    
    return dfd.promise;
}

Making Promises in ES2015

Revealing constructor pattern:

new Promise((resolve, reject) => {
  resolve(10);
  // or
  reject(new Error('Reason'));
});
p instanceof Promise;
p.constructor === Promise;
Object.getPrototypeOf(p) === Promise.prototype;

Now possible:

Consumption

  • No difference in consumption
  • Promises are anything "thenable", so there's interop
  •  
Promise.resolve('A')
    .then(a => $.Deferred().resolve(a + 'B').promise())
    .then(b => Q(b + 'C'))
    .then(c => console.log(c)) // ABC
    .catch(handleTheError)
  • DOM APIs can now return promises (e.g. crypto)
window.crypto.subtle.generateKey(...)
.then(function(key){
    console.log(key);
})
.catch(function(err){
    console.error(err);
});

Let's...

  • Get the top post from /r/javascript
  • Get this post's top comment
  • If anything fails, just tell the user why
try {
  let topPost = getTopRedditPost();
  console.log("Top Post: ", topPost.title);

  let topComment = getTopComment(topPost);
  console.log("Top Comment: ", topComment.body);
} 
catch(err) {
  console.log("Error: " + err);
}

If it were synch

function GET(url, cb) {
  var req = new XMLHttpRequest();
  req.open('GET', url);
  req.send();

  req.onreadystatechange = function () {
    if (req.readyState === 4) {
      if (req.status === 200) {
        cb(null, JSON.parse(req.responseText));
      } else {
        cb(new Error(`Failed Request with Status: ${req.status}`));
      }
    }
  };
}

GET('https://www.reddit.com/r/javascript/top.json', function (err, posts) {
  if (err) console.log(`Problem: ${err.message}`);
  var topPost = posts.data.children[0].data;
  console.log(`Top Post: ${topPost.title}`);

  GET(`https://www.reddit.com/r/javascript/comments/${topPost.id}.json`, function (err, comments) {
    if (err) console.log(`Problem: ${err.message}`);

    var topComment = comments[1].data.children[0].data;
    console.log(`Top Comment: ${topComment.body}`);   
  });
});

Async behaves like sync...

getTopRedditPost();
  .then(topPost => {
    console.log(`Top Post: ${topPost.title}`);

    return getTopComment(topPost)
      .then(topComment => {
        console.log(`Top Comment: ${topComment.body}`); 
      });
  })
  .catch(err => console.log(`Error: ${err.message}`));

function getTopRedditPost() {
  return GET('https://www.reddit.com/r/javascript/top.json')
    .then(posts => posts.data.children[0].data);
}

function getTopComment(post) {
  return GET(`https://www.reddit.com
              /r/javascript/comments/${topPost.id}.json`)
    .then(comments => comments[1].data.children[0].data);
}

try {
  var topPost = getTopRedditPost();
  console.log("Top Post: ", topPost.title);

  var topComment = getTopComment(topPost);
  console.log("Top Comment: ", topComment.body);
} 
catch(err) {
  console.log("Error: " + err);
}

... but doesn't really look like sync.

Now you're convinced promises are awesome

(if you weren't already)

More complicated promise-based workflows

Combination

  • jQuery: .when(...)
  • Q, Bluebird, ES2015: .all(...)
let namePromises = [
    GETPROFILE('sally'), 
    GETPROFILE('bob'), 
    GETPROFILE('sue')
];

Promise.all(names)
    .then(profiles => console.log(profiles.length)); // 3

// OR written more realistically with Map

let names = ['sally', 'bob', 'sue'];
Promise.all(names.map(name => GETPROFILE(name)))
    .then(profiles => console.log(profiles.length)); // 3

Let's...

  • Get the top 5 posts from /r/javascript
  • Get the top comment for each of the five
  • If anything fails, just tell the user why
try {
  let top5Posts = getTop5RedditPosts();
  for (let post of top5Posts) {
    console.log('Top Post:', post.title);

    let topComment = getTopComment(post);
    console.log('Top Comment:', topComment.body);
  }
} 
catch(err) {
  console.log('Error:', err);
}

If it were sync

Async behaves like sync...

getTop5RedditPosts()
  .then(topFivePosts => {
    return Promise.all(topFivePosts.map(post => {
      return getPostTopComment(post)
        .then(topComment => {
          console.log(`----\nTop Post: ${post.data.title}`);
          console.log(`Top Comment: ${topComment.body}`); 
        });
    }));
  })
  .catch(err => console.log(`Error: ${err.message}`));

function getTop5RedditPosts() {
  return GET('https://www.reddit.com/r/javascript/top.json')
    .then(posts => posts.data.children.slice(0, 5));
}

function getTopComment(post) {
  return GET(`https://www.reddit.com
              /r/javascript/comments/${topPost.id}.json`)
    .then(comments => comments[1].data.children[0].data);
}
try {
  let top5Posts = getTop5RedditPosts();
  for (let post of top5Posts) {
    console.log('Top Post:', topPost.title);

    let topComment = getTopComment(topPost);
    console.log('Top Comment:', topComment.body);
  }
} 
catch(err) {
  console.log('Error:', err);
}

... but it still doesn't look like sync.

Iterators

Iterators

  • Producer provides multiple values to producer
  • Producer has a next() method that returns { done, value } tuples.
  • Like promises, an iterator is anything with next()
iterface IIterable {
    [Symbol.iterator](): IIterator
}

interface IIterator {
    next(): IIteratorResult,
    throw()?: IIteratorResult
    return()?: IIteratorResult
}

interface IIteratorResult {
    value: T,
    done: boolean
}

Iterators

let arr = ['a', 'b'];
for (let pair of arr.entries()) {
    console.log(pair); // [0, 'a'] [1, 'b']
}
let arr = ['a', 'b', 'c'];
let itr = arr.entries();
itr.next(); // {"value":[0,"a"],"done":false}
itr.next(); // {"value":[1,"b"],"done":false}
itr.next(); // {"value":[2,"c"],"done":false}
itr.next(); // {"done":true}


let msg = "hi";
let iterator = msg[Symbol.iterator]();
 
iterator.next(); // { value: "h", done: false }
iterator.next(); // { value: "i", done: false }
iterator.next(); // { value: undefined, done: true }

Generators

Generators return Iterators

Use the function* syntax

function* producer() {
  yield 1;
  
  for (let i = 2; i <= 4; i++) {
      yield i;
  }  	
  
  return 5;
}
var iter = producer();
Array.from(iter); // ???
var iter = producer();
iter.next() // { done: false, value: 1 }
iter.next() // { done: false, value: 2 }
iter.next() // { done: false, value: 3 }
iter.next() // { done: false, value: 4 }
iter.next() // { done: true, value: 5 }
var iter = producer();
Array.from(iter); // [1,2,3,4] WAT - Iteration doesn't include return

Yield & Next

Yield pauses, returns yielded value to the consumer

function* producer() {
  yield 1;
}

producer().next(); // { value: 1, done: false }

Next() continues execution and takes an optional value

function* producer() {
  let a = yield 1;
  return a + 1;
}

let p = producer();

p.next(); // { value: 1, done: false }

p.next(2); // { value: 3, done: true }

Quiz

var producer = function*() {
  let a = yield 10;
  console.log('A', a);
  
  let b = yield a;
  console.log('B', b);
  
  return 30;
}

var iter = producer();
console.log('yielded', iter.next());
console.log('yielded', iter.next());
console.log('yielded', iter.next(100));
console.log('yielded', iter.next(200));
yielded {"value":10,"done":false}
A Undefined
yielded {"done":false}
B 100
yielded {"value":30,"done":true}
yielded {"done":true}

Spawn([generator])

Spawn takes a generator function. Every time the generator yields a promise, the execution of the generator is halted until the promise is resolved at which time spawn will call next() with the promise's resolved value, on the generator.

function spawn(gen) {
  let iter = gen();
  
  let p1 = iter.next().value;
  let p2 = p1.then(res => iter.next(res).value);

  return p2;
}

spawn(function*(){
  let a = yield Promise.resolve(10);
  return a + 20;
})
  .then(res => console.log(res)); // 30

Let's...

Rework our same scenario to use an iterator function, yield, and spawn.

(spawn(function*(){
  try {
    let posts = yield GET('https://www.reddit.com/r/javascript/top.json');
    let topFivePosts = posts.data.children.slice(0, 5);
    for (let post of topFivePosts) {
      let comments = yield GET(`https://www.reddit.com/r/javascript/comments/${post.data.id}.json`);
      let topComment = comments[1].data.children[0].data;
      console.log(`----\nTop Post: ${post.data.title}`);
      console.log(`Top Comment: ${topComment.body}`); 
    }
  } catch (ex) {
    console.log(`Error: ${ex.message}`);
  }
}))();

Q - .spawn()

Bluebird - .coroutine()

 

 

Generators + Promises + Spawn = ES2016 Async/Await

Async looks like sync!

let fetchTop5 = async function() {
  try {
      let top5Posts = await getTop5RedditPosts();
      for (let post of topFivePosts) {
        console.log(`----\nTop Post: ${post.data.title}`);

        let comment = await getTopComment(post);
        console.log(`Top Comment: ${topComment.body}`); 
      }
  } catch (ex) {
    console.log('Error:', err);
  }
};

fetchTop5();

function getTop5RedditPosts() {
  return GET('https://www.reddit.com/r/javascript/top.json')
    .then(posts => posts.data.children.slice(0, 5));
}

function getTopComment(post) {
  return GET(`https://www.reddit.com
              /r/javascript/comments/${topPost.id}.json`)
    .then(comments => comments[1].data.children[0].data);
}
let fetchTop5 = async function() {
  let posts = await GET('https://www.reddit.com/r/javascript/top.json');
  let topFivePosts = posts.data.children.slice(0, 5);
  for (let post of topFivePosts) {
    let comments = await GET(`https://www.reddit.com/r/javascript/comments/${post.data.id}.json`);
    let topComment = comments[1].data.children[0].data;
    console.log(`----\nTop Post: ${post.data.title}`);
    console.log(`Top Comment: ${topComment.body}`); 
  }

  return 'All done!'; // Here's that return value that was ignored
};

let fetching = fetchTop5()
  .then(res => console.log(res));

Async functions return a promise.  

The resolved value is the value returned.

By the way...

Now you're convinced promises are awesome and you will love them forever and ever and ever and ever.

(if you weren't already)

Use it all today with Babel

(stage <= 1 for async)

 

http://babeljs.io/docs/usage/experimental/

Thank You

 

 

 

@adamterlson

adam.terlson@gmail.com

JavaScript Promises in ES6 and Beyond

By Adam Terlson

JavaScript Promises in ES6 and Beyond

Learn about the changes in ES6 and ES7 that will make async code radically different. Introduces the concept of iterators, generators and walks slowly into async functions. Contains a host of solid, incremental examples.

  • 1,404