Hey Let's Learn About JavaScript Promises

Do you write JavaScript? If you do, you probably do a lot of asynchronous stuff. Here’s the thing about asynchronous stuff: typically we use callbacks, but there are these other things called Promises. Promises are better. Yes, better! Let's take a look at why.

JavaScript runs in a single-threaded event loop. Certain functions in JavaScript only add events to the event queue:

  • setTimeout
  • setInterval
  • addEventListener

These functions, and any functions using these functions, are said to operate asynchronously. When a function executes asynchronously, we can't directly use its return value. For instance, you may see code like this in Ruby:

response = HTTP.get("/things")
puts response.body

But you won't see similar code in JavaScript, because functions that perform IO are asynchronous and non-blocking.

var response = HTTP.get("/things") // This isn't possible
console.log(response.body)

In order to use values generated asynchronously, we need to be able to tell an asynchronous function what to do after the value is generated. One way to do this is through the use of a callback function.

Callbacks

A callback function is just a function that you pass as an argument to another function. That function then gets called when a task is complete.

For example, the setTimeout function takes two arguments. The first is the callback function, and the second is the amount of time to wait in milliseconds before calling the callback function:

setTimeout(function() { console.log("Done") }, 1000) // Prints done after 1 second

A common convention with callback functions is to handle both the error and the success cases of our asynchronous operation. Typically, the callback should handle an error as the first argument, and a value as the second argument.

The problem with using callbacks is that, if we have a long chain of asynchronous operations to perform, we end up with a deeply nested chain of callbacks. This long chain hinders our ability to understand the code, handle errors, and debug our program.

var waitFor = function(name, done) {
  console.log('Wating for ' + name)

  setTimeout(function() {
    if (name === 'Steve') {
      done(Error('Steve is always late!'))
    } else {
      done(null, name)
    }
  }, 500)
}

waitFor('Bob', function(error, bob) {
  if (error) {
    console.log(error)
  } else {
    waitFor('Joe', function(error, joe) {
      if (error) {
        console.log(error)
      } else {
        waitFor('Steve', function(error, steve) {
          if (error) {
            console.log(error)
          } else {
            waitFor('John', function(error, john) {
              if (error) {
                console.log(error)
              } else {
                console.log('Got ' + bob)
                console.log('Got ' + joe)
                console.log('Got ' + steve)
                console.log('Got ' + john)
                console.log("Ok, let's go!")
              }
            })
          }
        })
      }
    })
  }
})

// Wating for Bob
// Wating for Joe
// Wating for Steve
// [Error: Steve is always late!]

We can make handling asynchronous operations easier by using Promises.

Promises

Promise objects are native to JavaScript as of ECMAScript 6. A Promise object is a container for a value that will be generated by a potentially asynchronous operation. There are two main concepts to a Promise.

One, a Promise can be in one of three states:

  • pending
  • fulfilled
  • rejected

A pending Promise has not been fulfilled or rejected. A Promise becomes fulfilled once the operation completes successfully and a value has been associated to the Promise. A Promise becomes rejected once the operation has failed and an error is attached to the Promise. Promises can only be fulfilled or rejected once. Once a Promise is fulfilled it cannot be rejected, and vice-versa.

Two, a Promise is "thenable", meaning it has a method, then, that defines how to handle the value (or error) once it is known. One of the great benefits of the Promise object is that the handler provided by then is guaranteed to be called once attached if the Promise is already fulfilled or rejected. This removes the race condition between when an operation completes and when a handler for the value from the operation is attached.

A Promise becomes fulfilled by calling the resolve function. When resolve is called with a value, the Promise is fulfilled. When resolve is called with another Promise, the original Promise adopts the state of the other Promise once that Promise has been either fulfilled or rejected. In other words, resolving Promise A with rejected Promise B thereby makes Promise A rejected.

Constructor

The constructor is the entry point to converting an asynchronous operation that uses callbacks into one that returns a Promise.

function getBears() {
  return new Promise(function(resolve, reject) {
    Http.get('/bears', function(response) { // Our async operation with a callback
      if (response.isSuccess()) {
        resolve(response.body()) // The Promise gets fulfilled with a value
      } else {
        var error = Error("Request failed")
        reject(error) // The Promise gets rejected with an error
      }
    }
  })
}

Promise.reject()

The reject method returns an already rejected Promise:

Promise.reject(Error('Book not found'))

Promise.resolve()

The resolve method returns an already resolved Promise:

Promise.resolve({ id: 5, title: 'Moby Dick' }) // A fulfilled Promise
Promise.resolve(Promise.reject(Error('Book not found'))) // A rejected Promise

Promise.prototype.then()

The then method handles the resolved value of the Promise and returns a new Promise.

Promise.resolve(1).then(function(n) { return n + 1 })

The method will either wrap the returned value from the handler in a resolved Promise, or use the returned Promise.

Promise.resolve(1).then(function(n) { return Promise.resolve(n + 1) })

The handler provided to then is not called if the Promise is rejected.

Promise.reject('Error').then(JSON.parse) // Returns a rejected Promise, JSON.parse is not called

It optionally takes a second handler to handle a rejected Promise.

Promise.reject('Error').then(JSON.parse, console.log) // Prints 'Error'

This can also be done by using...

Promise.prototype.catch()

The catch method handles rejected Promises only.

Promise.reject('Error').catch(console.log)

It's the same as doing:

Promise.reject('Error').then(undefined, console.log)

The catch handler can resume the chain by returning a resolved Promise:

Promise.resolve('{\"message\":\"hello\"') // Resolve with invalid JSON
  .then(JSON.parse) // This handler fails
  .then(function(data) { // This handler is not called
    data.parsed = true
    return data
  })
  .catch(function(error) { // This handler is called
    return Promise.resolve({ message: 'Default message' }) // Returns a new resolved Promise
  })
  .then(function(data) { console.log(data.message) }) // This handler is called

Chaining

Promises solve our callback problem by allowing us to chain synchronous and asynchronous functions together in a flat manner while allowing us to handle errors anywhere in the chain.

var waitFor = function(name) {
  console.log('Waiting for ' + name)

  return new Promise(function(resolve, reject) {
    setTimeout(function() {
      if (name === 'Steve') {
        reject(Error('Steve is always late!'))
      } else {
        resolve(name)
      }
    }, 500)
  })
}

var waitForFriend = function(name) {
  return function() {
    return waitFor(name)
  }
}

var sayHey = function(name) {
  console.log('Hey ' + name)
}

var leave = function() {
  console.log("Ok let's go")
}

var gotImpatient = function(error) {
  console.log(error.message)
  return Promise.resolve("We're leaving")
}

waitFor('Bob')
  .then(sayHey)
  .then(waitForFriend('Joe'))
  .then(sayHey)
  .then(waitForFriend('Steve'))
  .then(sayHey)
  .then(waitForFriend('Mike'))
  .then(sayHey)
  .catch(gotImpatient)
  .then(leave)

// Waiting for Bob
// Hey Bob
// Waiting for Joe
// Hey Joe
// Waiting for Steve
// Steve is always late!
// Ok let's go

Promise.all()

We can also operate on collections of values returned from multiple asynchronous operations. The all method returns a Promise that is resolved once all of the provided Promises have resolved. The Promise is rejected if one of the provided Promises is rejected. The resolved value will be an array of the resolved values from the provided Promises. The rejected value will be the error of the first rejected Promise.

var waitForAll = function() {
  return Promise.all([
    waitFor('Chuck'),
    waitFor('Gary'),
    waitFor('Larry')
  ])
}

var gotFriends = function(friends) {
  console.log('Got ' + friends)
  return friends
}

var rentCar = function(friends) {
console.log("Waiting in line for a car...")

return new Promise(function(resolve, reject) {
  setTimeout(function() {
    if (friends.length > 5) {
      reject(Error("Sorry, we only have small cars"))
    } else {
      var car = { color: 'blue', kind: 'prius' }
      resolve(car)
    }
  }, 500)
})
}

var emergencyCar = function(error) {
  console.log(error.message)
  console.log("Borrowing my mom's van...")
  return Promise.resolve({ color: 'red', kind: 'minivan' })
}

var cruise = function(car) {
  console.log('Crusing in the ' + car.color + ' ' + car.kind)
}

waitForAll()
  .then(gotFriends)
  .then(rentCar)
  .catch(emergencyCar)
  .then(cruise)

// Waiting for Chuck
// Waiting for Gary
// Waiting for Larry
// Got Chuck,Gary,Larry
// Waiting in line for a car...
// Crusing in the blue prius

Promises are cool! They allow us to write asynchronous code that's easy to understand and easy to compose with other functions to produce our desired results.

References

Kevin Buchanan, Software Craftsman

Kevin Buchanan is biking around Chicago looking for donuts on his way to write code

Interested in 8th Light's services? Let's talk.

Contact Us