Skip to content

Instantly share code, notes, and snippets.

@ezekg
Forked from meagar/promises.md
Last active August 29, 2015 14:06
Show Gist options
  • Save ezekg/df0c12ecff9813fcd014 to your computer and use it in GitHub Desktop.
Save ezekg/df0c12ecff9813fcd014 to your computer and use it in GitHub Desktop.

##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?

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