☞ Continuations

Direct style 🔗


var firstUser = getUser({id: 1});
ui.renderUser(firstUser);
						

Continuation passing style 🔗


getUser({id: 1}, function(firstUser) {
  ui.renderUser(firstUser);
});
						

The function written in CPS receives the 'continuation' as an extra argument. When the CPS function has computed its result value, it "returns" it by calling the continuation function with this value as the argument.

async CPS: Very common in JS


runAsyncTask(taskInput, function(taskOutput) {
  console.log("task finished with output " + taskOutput);
});
						

Not all callbacks qualify as continuations


// This is pub-sub, not continuation passing
$('.someButton').on('mouseover', function() {
  tooltip.show();
});
						

☇ Motivation

  • A better alternative to async continuation passing. (Cause async CPS is actually pretty bad)
  • A way to regain useful control flow patterns only applicable to the direct (synchronous) style. (Cause we can't use them, with async CPS)

💡 Promise

A proxy for a value that will become available in the future.

(or that is already available. Or that will never be available. (edge cases))

Here's a promise for a user


var userPromise = $.get('http://api/users/1'); // jQuery 3.0, please
						

This promise is pending.

A user-promise is not a user


// Let's assume user objects look like {id: '..', name: '..', email: '..'}.

// This is an error
console.log(userPromise.name); // No 'name' attribute on userPromise object
						

So what can we do with it?

A pending promise will eventually be fulfilled with a value.

So what we can do is specify what should happen when it's fulfilled:


userPromise.then(function(user) {
  ui.renderUser(user);
});
							

Commonly, we omit the promise object and chain then directly onto the initial promise-returning function call


$.get('http://api/users/1').then(function(user) {
  ui.renderUser(user);
});
						

Here's another example.

Assume wait() is a promise-returning function


wait(60000).then(function() {
  alert("BOO!");
});
						

Note: Fullfilment handlers will run even if the promise is already fulfilled


var timeoutPromise = wait(60000);

timeoutPromise.then(function() {
  console.log('BOO');
  timeoutPromise.then(function() {
    console.log('HOO');
  });
});

// This will log 'BOO' and then 'HOO'
						

Just a glorified continuation?

How is this better than async continuation passing?


// Async CPS
runAsyncTask(taskInput, function(taskOutput) {
  console.log("task finished with output " + taskOutput);
});

// vs

// Promise
runAsyncTask(taskInput).then(function(taskOutput) {
  console.log("task finished with output " + taskOutput);
});
                        

👎 Callback APIs are inconsistent


$.ajax({
  url: 'http://..',
  success: function(data, textStatus, jqXhr) {/* use data */},
  error: function(jqXHR, textStatus, errorThrown) {/* handle error */}
});

http.request('http://..', function(error, response) {
  if (error) {/* handle error */}
  /* handle response (no error) */
});

require(['jquery', 'underscore'],
  function($, _) {/* make use of $ and _ */},
  function(error) {/* handle error */}
);
						

👍 Promise API


runAsyncTask(taskInput).then(function(taskOutput) {
  // Task output = promise's fulfillment value
});
						

$.ajax({url: 'http://..'}).then(function(data) {
  // use data
});

http.request('http://..').then(function(response) {
  // handle response
});

require(['jquery', 'underscore']).then(function(deps) {
  // make use of deps.$ and deps._
						

(We'll talk about errors in a bit ..)

👎 Trust issue: No guarantee that callbacks will be treated as continuation

'3rd party' code may 'choose' to invoke given callback multiple times. Or invoke the error callback and the success callback. Both very popular JS bugs. (node example).

👍 Promise guarantee:

When fulfilled, a promise:

  • must not transition to any other state.
  • must have a value, which must not change.

(Taken from the spec.)

👎 Trust issue: No guarantee that continuations will be invoked synchronously/asynchronously

Massively popular Stack Overflow question:

I tried to return the value from the success callback as well as assigning the response to a local variable inside the function and return that one, but none of those ways actually return the response.


function foo() {
  var result;

  $.ajax({
    url: '...',
    success: function(response) {
      result = response;
      // return response; // <- I tried that one as well
    }
  });

  return result;
}

var result = foo(); // It always ends up being `undefined`.
						

But what if we had this instead?


function foo() {
  var result;

  getData(options, function(data) {
    result = data;
  });

  return result;
}

var result = foo(); // Now, does this work?
						

Isaac Z. Schlueter, the node / npm guy has written many wize words on this.

👍 Promise guarantee:

A fulfillment handler must not be called until the execution context stack contains only platform code.

I.e. it must be called asynchronously

(Taken from the spec.)

'You Don't Know JS: Async & Performance' has an entire chapter dissing callbacks.

Just an incremental improvement on continuations?

Nope

Promises offer a way to regain useful control flow patterns only applicable to the direct (synchronous) style.

Promise chaining & transformations


$.get('http://api/users/1')
  .then(function(user) {
    ui.renderUser(user);
  })
  .then(function() {
    return wait(1000);
  })
  .then(function() {
    ui.renderMessage("You can now edit the user");
  });
						

This works because .then(..) always returns a promise.

How does chaining work, exactly?

A fullfilment handler may return a value that

is not a promise


$.get('http://api/users/1').then(function(user) {
  return user.name;
});
						

or is a promise


$.get('http://api/users/1').then(function(user) {
  return $.get('http://api/organizations/' + user.organization);
});
						

Fullfilment handler returns value that isn't promise

Then promise gets fulfilled with that value


// promise p will be fulfilled with the user name
//  at the time that the _original promise_ (the user promise) is fulfilled
var p = $.get('http://api/users/1').then(function(user) {
  return user.name;
});
						

Fullfilment handler returns a promise

Then promise adopts the state of (effectively becomes) the returned promise


// promise p will be fulfilled with the user organization
//  at the time that the last API call completes
var p = $.get('http://api/users/1').then(function(user) {
  return $.get('http://api/organizations/' + user.organizationId);
});
						

Let's decompose the original example


var userPromise = $.get('http://api/users/1');

var postRenderPromise = userPromise.then(function(user) {
  ui.renderUser(user);
  // Implicitly returns undefined. So postRenderPromise is fulfilled
  //  with undefined 'immediately after' userPromise is fulfilled
});

var pausePromise = postRenderPromise.then(function() {
  return wait(1000);
  // Returns the promise that wait returns. So pausePromise is pending
  //  and will be fulfilled '1s after' postRenderPromise
})

var endPromise = pausePromise.then(function() {
  ui.renderMessage("You can now edit the user");
  // Implicitly returns undefined. So endPromise is fulfilled
  //  with undefined 'immediately after' pausePromise is fulfilled
});
						

CPS → (promises) → 'DS', pt 01:

Executing tasks, sequentially

Executing async tasks sequentially, in CPS

AKA the pyramid of doom


// Get the admin of the organization that the 'first user' belongs to
var admin;
getUserById(1, function(user) {
  getOrgById(user.orgId, function(org) {
    getUserById(org.adminId, function(adminUser) {
      admin = adminUser;
    });
  });
});
						

Executing async tasks sequentially, in DS


var user = getUserById(1);
var org = getOrgById(user.orgId);
var admin = getUserById(org.adminId);
						

Executing async tasks sequentially with promises


var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});
						

(This is more common though:)


var adminPromise = getUserById(1)
  .then(function(user) {
    return getOrgById(user.orgId);
  })
  .then(function(org) {
    return getUserById(org.adminId);
  });
						

Async continuations & Error handling (exceptions)

(TL;DR: I doesn't work)

Error handling, in DS


var admin;

try {
  var user = getUserById(1);
  var org = getOrgById(user.orgId);
  admin = getUserById(org.adminId);
} catch (exception) {
  ui.renderError(exception);
  return;
}

// Continue with happy path, make use of admin reference
						

Async CPS version. Wouldn't it be nice if


var admin;
try {
  getUserById(1, function(user) {
    getOrgById(user.orgId, function(org) {
      getUserById(org.adminId, function(adminUser) {
        admin = adminUser;
      });
    });
  });
} catch (exception) {
  ui.renderError(exception);
  return;
}

// Continue with happy path, make use of admin reference
						

sadly, this doesn't work due to the continuations executing (and the exceptions being thrown) within call stacks that aren't the original catch-block-containing call stack.

So we have to do this instead


var admin;
try {
  getUserById(1, function(user) {
    try {
      getOrgById(user.orgId, function(org) {
        try {
          getUserById(org.adminId, function(adminUser) {
            admin = adminUser;
          });
        } catch (exception) {
          ui.renderError(exception);
        }
      });
    } catch (exception) {
      ui.renderError(exception);
    }
  });
} catch (exception) {
  ui.renderError(exception);
  return;
}
						

:(

Promises & Error handling (exceptions)

So how do promises facilitate error handling? We've already seen how

A pending promise will eventually be fulfilled with a value.

Well ..

A pending promise will eventually be fulfilled with a value.

A pending promise will either

  • be fulfilled with a value. Or
  • be rejected with a reason.

(or remain pending forever - not interesting)

Enter the 'rejection handler'


userPromise.then(function(user) { // Both fulfillment & rejection handlers
  ui.renderUser(user);
}, function(error) {
  ui.renderError(error);
});
                        

userPromise.then(null, function(error) { // No fulfillment handler
  ui.renderError(error);
});
                        

Chaining & transformations, revisited


var transformedPromise = originalPromise.then(fulHanlder, rejHandler);
                        
  • If the invoked handler returns a value transformedPromise gets resolved with that value: It's either fulfilled with a returned non-promise value or adopts the state of a returned promise.
  • If the invoked handler throws an exception, transformedPromise is rejected with that exception (as the reason).

For example


$.get('http://api/users/1')
  .then(function(user) {
    if (user.name !== 'root') {
      throw new Error('Unexpected non-root user');
    }
    ui.renderUser(user);
  })
  .then(function() {
    return wait(1000);
  })
  .then(function() {
    ui.renderMessage("You can now edit the user");
  })
  .then(null, function(error) {
    ui.renderError(error);
  });
                        

Beware!


$.get('http://api/users/1')
  .then(function(user) {
    if (user.name !== 'root') {
      throw new Error('Unexpected non-root user');
    }
    ui.renderUser(user);
  }, function(error) {
    // This rejection handler is called if the $.get promise
    //  is rejected. But is _not_ called for the unexpected-user error
  })
  .then(null, function(error) {
    // This rejection handler is called for _any_ error
  });
                        

CPS → (promises) → 'DS', pt 02:

Executing tasks, sequentially, with error handling

We've already seen (no error handling here):


var user = getUserById(1);
var org = getOrgById(user.orgId);
var admin = getUserById(org.adminId);
                        

var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});
                        

with error handling


var admin;

try {
  var user = getUserById(1);
  var org = getOrgById(user.orgId);
  admin = getUserById(org.adminId);
} catch (exception) {
  ui.renderError(exception);
  return;
}

// Continue with happy path, make use of admin reference
                        

var user = getUserById(1);
var org = user.then(function(user) {return getOrgById(user.orgId);});
var admin = org.then(function(org) {return getUserById(org.adminId);});

admin.then(function(admin) {
    // Continue with happy path, make use of admin reference
}, function(error) {
    ui.renderError(exception);
})
                        

Bonus Material (TODO)

  • Creating promises in Q, Bluebird, ES6

    • Implementing wait()
    • Implementing a formSubmittedPromise
  • Syntax improvements with .bind()
  • Exception gotchas - the .done() function
  • How to 'fix' jQ < 3 (non-conformant in general) promises
  • Executing tasks in parallel (the .all() (& the .any() function))
  • Call stacks

    • Why does exception handling not work in async contexts
    • The promise guarantee '.. execution context stack contains only platform code'
  • Long stack traces for debugging

Where to go from here