Promises and ES6

A Quick Introduction

by Soares Chen

A talk.js presentation

October 2014

Current State

var readJson = function(filename, callback) {
  fs.readFile(filename, function(err, content) {
    if(err) return callback(err)

    try {
      var json = JSON.parse(content)
    } catch(err) {
      return callback(err)
    }

    return callback(null, json)
  })
}

Problems of Callback Hell

  • Nested callback spaghetti code
  • Callback called twice
  • Explicit error propogation
    • if(err) return callback(err)
  • Uncaught exception thrown to global scope

Enter Promises/A+

Basic Idea of Promise

  • An object representing future value
  • Can be returned by functions
  • Specify on what to do when the future value arrives
  • Specifying future action on promise gives a new promise
  • Implicit error propogation
  • Built on top of callback primitive

Promise States

  • Pending
  • Fulfilled
  • Rejected
     
  • Resolve - changing the promise from pending to either fulfilled or rejected state

new Promise()

var readFile = function(filename) {
  return new Promise(function(resolve, reject) {
    fs.readFile(filename, function(err, content) {
      if(err) return reject(err)
    
      resolve(content)
    })
  })
}

.then() and .catch()

// promise.then(onFulfilled, onRejected)
readFile('foo.json').then(function(content) {
  ...
}, function(err) {
  ...
})

// promise.then(null, onRejected)
//   = promise.catch(onRejected)
readFile('foo.json').then(function(content) {
  ...
}).catch(function(err) {
  ...
})

resolve()

var foo = function() {
  return new Promise(function(resolve, reject) {
    resolve('foo')
    resolve('bar')
    reject(new Error())
  })
}

foo().then(function(result) {
  // prints 'foo() returns foo'
  console.log('foo() returns', result)
})

reject()

var bar = function() {
  return new Promise(function(resolve, reject) {
    reject(new Error('error creating bar'))
  })
}

bar().then(function(result) {
  // should never print
  console.log('bar result:', result)
}, function(err) {
  console.log('oops! failed to create bar')
})

throw

var baz = function() {
  return new Promise(function(resolve, reject) {
    throw new Error('Yay! Can throw again!')
  })
}

bar().catch(function(err) {
  console.log('Caught error thrown in baz()')
})

Except Not Always..

var baz = function() {
  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      throw new Error('Async error is not caught!')

      resolve('never reach')
    }, 1000)
  })
}

bar().then(function(result) {
  console.log('Neither result nor')
}, function(err) {
  console.log('error callback will ever be called')
})

Explicit resolve()

var foo = function() {
  return Promise.resolve('foo')
}

var bar = function() {
  return Promise.reject(new Error('bar error')
}

resolve() != fulfill()

var baz = function() {
  return Promise.resolve(
    Promise.reject(new Error('baz error'))
}

baz().then(function(result) {
  console.log('never reach')
}, function(err) {
  console.log('final resolved value of baz() is error')
})

resolve() usage

var cached = null

var expensiveTask = function() {
  if(cached) return Promise.resolve(cached)

  return doExpensiveTask().then(function(result) {
    cached = result
    return result
  })
}

Normalize with resolve()

var compose = function(f1, f2) {
  return function() {
    return Promise.resolve(f2()).then(f1)
  }
}
  • f1 and f2 returns either immediate or promised value
  • resolve() converts any immediate value to promised value

.then() Chaining

// Sync version
baz(bar(foo(123)))

// Promised version
foo(123).then(bar).then(baz)

// Equivalent to
foo(123).then(function(fooResult) {
  return bar(barResult).then(function(barResult) {
    return baz(barResult)
  })
})

// Useful Example
readFile('foo.json').then(JSON.parse)
.then(function(json) { ... })

Nested Promises

foo(123).then(function(fooResult) {
  return bar(345, fooResult).then(function(barResult) {
    return {
      foo: fooResult,
      bar: barResult
    }
  })
}).then(function(result) {
  // Will print { "foo": ..., "bar": ... }
  console.log('result:', result)
})

catch() Error in Chains

foo(123).then(function(fooResult) {
  throw new Error('foo error')
})
.then(bar) // bar is never called
.catch(function(err) {
  console.log('Will catch all possible errors')

  // More commonly should rethrow error
  // throw err
  return 'recovered value'
})
.then(function(result) {
  // will print 'recovered value'
  console.log('final result:' result)
})

then(..., onRejected) gotchas

foo(123).then(function(fooResult) {
  throw new Error('Error is ignored')
}, function(err) {
  console.log('will only catch foo() errors')
})

Promise.all()

var readFiles = function(filenames) {
  return Promise.all(filenames.map(readFile))
}

Use Promise Today

Promise with ES6

using Traceur

ES6 Arrow Function

var array = [1, 2, 3]

// ES5
var mapped = array.map(function(x) {
  return x*x
})

// ES6
var es6 = array.map(x => x*x)

Arrowed Promise 

var readFile = filename =>
  new Promise((resolve, reject) =>
    fs.readFile(filename, (err, filepath) => {
      if(err) return reject(err)
      
      resolve(filepath)
    })

Timeout Promise

var timeout = time =>
  new Promise(resolve =>
    setTimeout(resolve, time))

timeout(1000).then(() => {
  ...
})

Promisify

var readFile = promisify(fs.readFile)

readFile(filename).then(content => {
  ...
})

Promisify Implementation

var promisify = fn =>
  (...args) =>
    createPromise((resolve, reject) =>
      fn(...args, (err, result) =>
        err ? reject(err) : resolve(result)))

Promisify Methods

var memcachedAsync = new Memcached(...)
var memcachedPromised = promisifyMethods(
  memcachedAsync, ['get', 'set', ...])

memcached.get('foo').then(data => {
  ...
})

Promisify Methods Implementation

var promisifyMethod = (object, method) =>
  promisify((...args) =>
    object[method](...args))

var promisifyMethods = (object, methods) =>
  methods.reduce((result, method) => {
    result[method] = promisifyMethod(object, method)
    return result
  }, { })

Error Handling Gotchas

doSomeComplicatedTasks().then(() => {
  ...
}).catch(err =>
  ... // try to recover
  throw new Error('error in middle of recovery!')

  console.log('never reach')
})

Double Catch Promise

doSomeComplicatedTasks().then(() => {
  ...
}).catch(err => {
  ... // try to recover
  throw new Error('error in middle of recovery!')

  console.log('never reach')
}).catch(err => {
  console.log('Something went seriously wrong! Exiting..')

  process.exit(1)
})

ES7 Async

  • https://github.com/lukehoban/ecmascript-asyncawait 
var readJson = async function(filename) {
  var content = await readFile(filename)
  
  return JSON.parse(content)
}

ES6 Async

var readJson = async(function*(filename) {
  var content = yield readFile(filename)

  return JSON.parse(content)
})

Async Implementation

var runAsync = gen => {
  var doNext = action => {
    try {
      var { done, value } = action()
    } catch(err) {
      return Promise.reject(err)
    }

    if(done) return value

    return Promise.resolve(value).then(
      result => doNext(() => gen.next(result)),
      err => doNext(() => gen.throw(err)))
  }

  return Promise.resolve(doNext(() => gen.next()))
}

var async = fn =>
  (...args) => runAsync(fn(...args))

Async Gotchas

// Using for loop
var readJsons = async(function*(filenames) {
  var results = []

  for(var i=0; i<filenames.length; i++) {
    var content = yield readFile(filenames[i])
    results[i] = JSON.parse(content)
  }

  return results
})

// Can't use Array.map()
var readJsons = async(function*(filenames) {
  return filenames.map(function() {
    // Error: invalid keyword yield
    var content = yield readFile(filename)
    return JSON.parse(content)
  })
})

Nested Async

var readJsons = async(function*(filenames) {
  var jsons = yield Promise.all(filenames.map(
    async(function*(filename) {
      var content = yield readFile(filename)
      return JSON.parse(content)
    })))

  return jsons
})
  • Not the most beautiful code
  • Break down problems into multiple async functions

Thank You

Soares Chen
soares.chen@gmail.com
https://github.com/soareschen

ES6 Promises

By Soares Chen

ES6 Promises

  • 2,511