Skip to content

Instantly share code, notes, and snippets.

@meagar
Last active December 18, 2015 00:49
Show Gist options
  • Save meagar/5699598 to your computer and use it in GitHub Desktop.
Save meagar/5699598 to your computer and use it in GitHub Desktop.
Promises.md

##JavaScript isn't threaded

  • Synchronous code locks up the browser
  • Asynchronous code frees up the browser but leads to...

##Async callback hell

"Return" values are no longer available to our synchronous code

$.get "/users", 
  success: (users) ->
    # Are visits available yet?
  
$.get "/visits",
  success: (visits) ->
    # Are users available yet?

"Watch" variables

gotUsers = false, gotVisits = false;

successHandler = ->
  if gotUsers && gotVisits
    console.log("Fetched!")

errorHandler = -> console.error "Something bad happened"

$.get "/users", 
  success: ->
    gotUsers = true
    successHandler()
  error: errorHandler
  
$.get "/visits",
  success: ->
    gotVisits = true
    successHandler()
  error: errorHandler

##We can "fix" this with nesting...

$.get "/users", 
  success: ->
    $.get "/visits", 
      success: ->
        console.log("Fetched!")
      error: errorHandler
  error: errorhandler

##... But we've traded one hell for another

$.get "/users"
  success: (users) ->
    for user in users
      $.get "/users/#{user.id}/visits"
        success: (visits) ->
          $.get "/users/#{user.id}/visits/#{visit.id}/directions",
            success: (directions) ->
              $.get "/users/#{user.id}/visits/#{visit.id}/work_report
                success: ->
                  console.log "Fetched!"
                error: errorHandler
            error: errorHandler
        error: errorHandler
    error: errorHandler

It gets worse

class Fetcher
  fetch: (options) ->
    @_fetchUsers
      success: (users) ->
        @_fetchVisits()
          success: (visits) -> options.success?(users, visits)
      error: ->
        # Handle errors
        options.error?()

  _fetchUsers = (options) ->
    $.get "/users"
      success: (user) ->
        @_usersFetched = true
        options.success?(users)
      error: (what) ->
        options.error?(what)
        
  _fetchVisits: (users, options) ->
    $.get "/visits"
      success: (visits) ->
        @_visitsFetched = true
        options.success?(visits)
      error: (what) ->
        options.error?(what)
        
new Fetcher().fetch
  success: ->
    # ...

What happened to exceptions?

It was easy to interrupt synchronous code and handle errors

try
  users = $.get "/users"
  visits = $.get "/visits"

  @visitsCollection.find(...)...

  # ...

  user.save()

  # ...

  console.log "Fetched!"
catch error
  console.error "Something bad happened!"

Enter promises

Promises...

  • Are objects returned by asynchronous functions
  • Start out pending
  • Become fulfilled or rejected at some point in the future
  • Allow binding of callbacks to fulfillment/rejection events
  • Have a then method

jQuery Promises...

  • Are already provided by AJAX calls
  • Use .done and .fail to handle fulfillment/rejection events
  • Can be used in custom code via $.Deferred() (more later)

Usage

Instead of callbacks going in, promises come out

userPromise = $.get "/users" # userPromise is "pending"

userPromise.done (response) ->
  console.log "userPromise was resolved!"
  
userPromise.fail (error) ->
  console.log "userPromise was rejected!"

Methods can be chained for a convient syntax

CoffeeScript

$.get "/users"
.done (records) ->
  console.log "Have users!"
.fail (error) ->
  console.error "Something bad:", error

JavaScript

$.get("/users")
.done(function (records) {
  console.log("Have users!")
).fail(function(error) {
  console.error("Something bad:", error)  
});

But is this an improvement?

##Before:

$.get "/users", 
  success: (users) ->
    $.get "/visits", 
      success: (visits) ->
        console.log("Fetched!")
      error: errorHandler
  error: errorhandler

After:

$.get "/users", 
.done (users) ->
  $.get "/visits", 
  .done (visits) ->
    console.log("Fetched!")
  .fail errorHandler
.fail errorhandler

The real awesomeness of promises: then

.then

The beauty of promises is that then chains together multiple promises...

promise1.then(promise2).then(promise3).then....

.then ...

... bubbling success/failure state through the chain ...

promise1.then(promise2).then(promise3).then...
.done ->
  console.log("All promises resolved!")
.fail ->
  console.log("One promise was rejected!")

then

... jumping immediately to our error handler, just like exceptions

promise1.then ->
  $("#loadingSpinner").show() # no promise
.then ->
  doSomethingLater() # returns promise2
.then ->
  doAnotherThingLater() # returns promise3
.then ->
  console.log "Half done!" # no promise
.then ->
  doAnotherThingAgain() # returns promise4
.then ->
  console.log "Done!"
  $("#loadingSpinner").hide()  # no promise
.fail ->
  console.log "promise1 OR promise2 OR promise3 OR promise4 failed!"

A slightly more real example

Pam.Loading.show() # Show spinner

$.get "/users" # success!
.then ->
  $.get "/visits" # success!
.then ->
  $.get "/404" # Error!
.then ->
  $.get "/directions" # skipped
.then ->
  console.log "Fetched!"
.fail ->
  console.error "Failed!"
.always ->
  Pam.Loading.hide()

Details

Promise.then

  • accepts a success callback and failure callback

    promise.then(successFn, failureFn)
  • invokes successFn on success, or failureFn on failure

  • returns a promise, which is resolved with the value of the success/failure callbacks

    promise2 = promise.then(successFn, failureFn)

then

  • if successFn or failureFn return a promise, promise2 becomes that promise

    promise2 = promise.then -> $.get("/url")
  • promise2 is now (in effect) the promise returned by $.get

then gives us async "exceptions"

X, Y, Z are async functions returning promises

X().then =>
  Y()
.then =>
  Z()
.then =>
  console.log "Success!"
.fail =>
  console.error "X or Y or Z failed!"

try/catch are great, where's finally?

X()
.then =>
  Y()
.then =>
  Z()
.then =>
  console.log "Success!"
.fail =>
  console.error "X or Y or Z failed!"
.always =>
  console.log "I run regardless!"

Promises and CoffeeScript work really well together

class Fetcher
  fetch: ->
    # Promise "falls off" the end of the method
    $.get("/users")
    
new Fetcher().fetch().done ->
  console.log "Done!"

Callbacks can be added at any level:

and are guaranteed to run in the correct order!

class Fetcher
  fetch: ->
    # Promise "falls off" the end of the method
    $.get("/users").done (users) =>
      # I run first!
      @users = users

f = new Fetcher()

f.fetch().done ->
  # I run second!
  console.log "Users:", f.users

Our earlier asyncronous example...

class Fetcher
  fetch: (options) ->
    @_fetchUsers
      success: (users) =>
        @_fetchVisits()
          success: (visits) -> options.success?(users, visits)
      error: ->
        # Handle errors
        options.error?()

  _fetchUsers = (options) ->
    $.get "/users"
      success: (user) =>
        @_usersFetched = true
        options.success?(users)
      error: (what) ->
        options.error?(what)
        
  _fetchVisits: (users, options) ->
    $.get "/visits"
      success: (visits) =>
        @_visitsFetched = true
        options.success?(visits)
      error: (what) ->
        options.error?(what)
        
new Fetcher().fetch
  success: ->
    # ...

... is dramatically improved ...

class Fetcher
  fetch: ->
    @_fetchUsers().then =>
      @_fetchVisits()



  _fetchUsers = ->
    $.get ("/users").done (users) =>
      @_usersFetched = true

  _fetchVisits: ->
    $.get("/visits").done (visits) =>
      @_visitsFetched = true

... and easily extended

class Fetcher
  fetch: ->
    @_fetchUsers().then =>
      @_fetchVisits()
    .then =>
      @_fetchPretzels()

  _fetchUsers = ->
    $.get ("/users").done (users) =>
      @_usersFetched = true

  _fetchVisits: ->
    $.get("/visits").done (visits) =>
      @_visitsFetched = true
      
  _fetchPretzels: ->
    $.get("/pretzels")

Making our own promises

jQuery promises have two sides

  • a "private" interface, called a "deferred"
    • exposes resolve/reject/done/fail
  • a "public" interface, called a "promise"
    • exposes only done/fail

Deferreds

  • Created via $.Deferred(), called dfd by convention
  • Fulfilled internally by resolve(...)
  • Rejected internally by reject(...)
  • Watched publicly for success via .done(callback)
  • Watched publicly for failure via .fail(callback)

Promises

  • Provide done and fail callbacks
  • Have the all-important then method
  • Prevent calling code from resolving/rejecting a promise which isn't theirs to manage

Internally...

Your class/function makes a new deferred, returns its promise

myAsyncFunction = ->
  dfd = $.Deferred()
  
  # Resolve our deferred in 1 second
  setTimeout (-> dfd.resolve("Time's up!")), 1000
  
  # Return the "public" interface to our deferred
  dfd.promise()

Externally...

Your calling code receives a promise and waits for it to resolve:

myAsyncFunction()
.done (message) ->
  alert(message) # Time's up!
.fail (exception) ->
  alert("Something bad!")
  console.error(exception)

On style...

Don't over-use your own deferreds!

Bad:

myFunction: (url) ->
  dfd = $.Deferred()
  
  $.get url,
  .done ->
    # Churn through results and then...
    dfd.resolve.apply(dfd, arguments)
  .fail ->
    dfd.reject.apply(dfd, arguments)
    
  dfd.promise()

... When existing promises will serve:

Good:

myFunction: (url) ->  
  $.get url,
  .done ->
    # Churn through results and then...    

when

Bundles up several promises into a new promise:

$.when($.get("/users"), $.get("/visits")).then (users, visits) =>
  console.log "Fetched users", users[0], "and visits", visits[0]
.fail =>
  console.error "Failed!"

The new promises fails or succeeds based on the sub-promises

Don't abuse it!

No!

$.when(myFunction()).then =>
  #...

Yes!

myFunction().then =>
  # ...

When to when?

saveModels = ->
  dfd = $.Deferred()

  for model in myModels
    model.save() # returns a promise

  # ... ???

  dfd.promise()

when.apply is great

saveModels = ->
  dfds = []

  for model in myModels
    dfds.push model.save() # returns a promise

  $.when.apply(@, dfds).promise()

Questions?

@meagar
Copy link
Author

meagar commented Oct 18, 2013

This is a quick presentation I did for my office, to get people excited about using Promises in our CoffeeScript.

Requires GistDeck.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment