A Promised Talk

Writing simple, readable, and maintatable asych JavaScript

by Mark Bennett

Who am I?

JavaScript = Asynchronous Code

The problem

              
  var files = ['a.json','b.json','c.json'],
      resources = [];

  files.forEach(function(file) {
    loadFile({
      success: function(content) {
        resources.push(content);
        if (files.length === resources.length) {
          // resources loaded
          createServer(function(server) {
            server.on_request = function(req) {
              // Handle request...
            };
            server.listen(function() {
              app.registerServer(server, function(app, err) {
                if (err !== undefined) {
                  app.error("Unable to register that this server is running.");
                }
              });
            }, function(msg) {
              app.error("Unable to start server! " + msg); 
            });
          }, function(error) {
            app.error("Can't create the server");
          });
        }
      },
      error: function(msg) {
        app.error("Unable to load file:" + msg);
      }
    });
  });
              
            

Issues

  1. Readability
  2. Error handling
  3. Coupling

Promises

Promise A+ Spec

A promise represents a value that may not be available yet. The primary method for interacting with a promise is its then() method.

A promise of a future value

  • Unresolved - no value yet
  • Resolved - the promise is fullfilled
  • Rejected - the promise was a failure

An example

              
var a_promise = retrieveFile();
a_promise.then(function(file) {
  // The awesome goes here
});
              
            

Handle rejection well

              
var a_promise = retrieveFile();
a_promise.then(function(file) {
  // The awesome goes here
},
function(rejection_msg) {
  // Optional, error handling for when the promise is rejected
});
              
            

Chain together promises

* Notice the output of the first, becomes the input to the second

              
retrieveFile().then(function(file) {
  return "returned from first promise";
}.then(function(input) {
  input === "returned from first promise";
});
              
            

Save a promise for later

              
var promise = retrieveFile();

// ..later on that same day

promise.then(function(file) {
  // Even if the promise was resolved a long time before
  // you can still add a then() to access it's value.
  // Try doing that with a callback!
});
              
            

You can also make your own promises

              
function promised_val(input) {
  // Create a deferred to handle the promised value
  var deferred = Q.defer();

  // do work
  setTimeout(function() {
     deferred.resolve("Awesome, " + input + "!");
  }, 1000);

  // return the promise
  return deferred.promise;
}

promised_val("sauce").then(function(val) {
  console.log(val);
});
              
            

Chain together new promises

              
retrieveFile().then(function(file) {
  // Triggered after the file is retrieved
  var deferred = Q.defer(),
      promise = deferred.promise;

  setTimeout(function() {
    deferred.resolve("A promised value");
  }, 5000);

  return promise;
}.then(function(val) {
  // Logs "A promised value" after 5 seconds
  console.log(val);
});
              
            

Manipulate promises

              
var array_of_values   = [1, 2, 3],
    array_of_promises = array_of_values.map(promise_returning_func);

Q.all(array_of_promises).
then(function(array_of_outputs) {
  // Called after all the promises are fullfilled,
  // with an array of the values from each promise
}).fail(function() {
  // Triggered if any of the promises in the array fail
});
              
            

Promise Implementations

Improving readability

Replace callback with functions returning promises

              
function loadFile(file_path)
function createServer(content)
server.prototype.listen = function()
app.registerServer = function(server)
              
            
              
var files = ['a.json','b.json','c.json'];

Q.all(files.map(loadFile)).
then(
  function(content) {
    // resources loaded
    return createServer(content);
  },
  function(msg) {
    app.error("Unable to load file:" + msg);
  }
).
then(
  function(server) {
    // Server created
    server.on_request = function(req) {
      // Handle request...
    };
    return server.listen();
  }, 
  function(err_msg) {
    app.error("Can't create the server");
  }
).
then(
  function(server) {
    // Server is listening
    return app.registerServer(server);
  },
  function(err_msg) {
    app.error("Unable to start server! " + msg); 
  }
).
then(undefined, function(err_msg) {
  app.error("Unable to register that this server is running.");
});
              
            

Improvements

  1. Linear story
  2. Less nesting
  3. File loading progress
  4. Captures thrown exceptions

Issues

  1. Too many callbacks
  2. A bit longer

Error Handling

The code

              
var files = ['a.json','b.json','c.json'];

Q.all(files.map(loadFile)).
then(function(content) {
  // resources loaded
  return createServer();
}).
then(function(server) {
  // Server created
  server.on_request = function(req) {
    // Handle request...
  };
  return server.listen();
}).
then(function(server) {
  // Server is listening
  return app.registerServer(server);
}).
fail(function(err_msg) {
  app.error("Unable to initialize server. Root cause = " + err_msg);
});
              
            

What happened?!

One fail() = many errors

              
try {
  createServer();
  server.on_request = function(req) { /* snip */ };
  server.listen();
  app.registerServer(server);
} catch (err) {
  app.error("Unable to initialize server. Root cause = " + err_msg);
}
              
            

Where next?

Questions?

Tweet at @MarkBennett