Comparing promises with StratifiedJS

March 11, 2014 by Tim Cuthbertson

We often get asked how StratifiedJS relates to (and interoperates with) other methods of managing concurrency in JavaScript. There's also been a lot of buzz recently about promises in JavaScript, as they are making their way into browsers - html5rocks has an excellent tutorial explaining what promises are, and how to use them. We were asked on twitter if it made sense to show some of the same examples from this tutorial if they were written in SJS, and we think it does.

We're not going to go over the full tutorial here. The promises tutorial covers a lot of introductory ground, which is useful reading about the basics of JavaScript concurrency (even if you're not using promises). Instead, we'll mostly be talking about the differences in how you'd go about the same sort of tasks with promises and with StratifiedJS.

Writing code with promises:

Many of the functions you'll use from the promise API are there to give you a subset of JavaScript control-flow for asynchronous operations. For example:

  • expression chaining:

    • JavaScript: makeB(makeA());
    • Promise: makeA().then(function(a) { return makeB(a); });
  • statement chaining:

    • JavaScript: makeA(); makeB();
    • Promise: makeA().then(makeB);
  • exception handling:

    • JavaScript: try { makeA(); } catch(e) { ... };
    • Promise: makeA().catch(function(e) { ... });

As a concrete example, here's a piece of code using regular JavaScript control flow:

Synchronous JavaScript code
// This code can't actually work
// since `get()` is asynchronous
function getStory() {
  try {
    var response = get('story.json');
    console.log("Success!", response);
    return response;
  } catch(error) {
    console.log("Failed!", error);
  }
};

... and here's how you would rewrite it to use promise-based control flow:

Promises
function getStory() {
  return get('story.json').then(function(response) {
    console.log("Success!", response);
    return response;
  }).catch(function(error) {
    console.log("Failed!", error);
  });
}

In terms of clarity and composability, promises can provide a big step up from callback-based asynchronous methods. Rather than arranging for an ephemeral function to be invoked at some later point, promises let you represent incomplete computations as actual values that can be stored in variables, passed around to other functions, and manipulated like any other object.

… in terms of the syntax you wish you could write, promises are still largely an inconvenience

But in terms of the syntax you wish you could write, promises are still largely an inconvenience. In the above example, you would not choose to return a promise if getStory() were actually synchronous - all it would do is add inconvenience and require users of your function to use the promise API to extract the real result of your function.

Letting the computer do the work for you

Since promises effectively re-implement normal JavaScript control-flow constructs as functions, it's worth considering whether this translation can be automated - i.e you write getB(getA()), and the computer "does the right thing" for you - if getA() returns a value then getB() is called immediately, but if getA() returns a promise then getB() will be queued to execute once the value of a is actually ready.

Similarly, when you transform a synchronous method from:

Synchronous JavaScript code
function getFoo(input) {
  var res = getResource(input);
  try {
    return res.get('foo');
  } catch(e) {
    console.warn("Failed to get a");
    return null;
  } finally {
    res.close();
  }
}

to:

Promises
function getFoo(input) {
  return getResource(input).then(function(res) {
    return res.get('foo').catch(function(e) {
      console.warn("Failed to get a");
      return null;
    }).then(function() {
      res.close();
    });
  });
}

It's worth asking: can't the computer perform this transformation for us?

Practically speaking, this is exactly what StratifiedJS does (but that's by no means all it does). It doesn't actually use a promise API under the hood, but the result is the same.

Converting the story example

To give a concrete comparison, here's the final Promise-based code for the demo from that html5rocks article (it's down the bottom of the "Parallelism and sequencing - Getting the best of both" section):

Promises
getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence.then(function() {
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
});

.. and here's the most straightforward transformation into StratifiedJS:

StratifiedJS
try {
  var story = getJSON('story.json');
  addHtmlToPage(story.heading);

  // initiate getJSON calls in parallel using `transform.par`,
  // which maintains the order of values.
  var chapters = transform.par(story.chapterUrls, getJSON);

  // Iterating over the results with `each` will
  // cause the loop to block until each successive
  // value is ready (i.e it waits for the next chapter)
  each(chapters, function(chapter) {
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
} catch(err) {
  addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none';
For new users wondering what new syntax you need to write asynchronous code in StratifiedJS, the answer is frequently none

For new users wondering what new syntax you need to write asynchronous code in StratifiedJS, the answer is frequently none (as in the code above) - you just write synchronous style JavaScript syntax, and StratifiedJS takes care of handling the asynchronous behaviour under the hood.

Of course, StratifiedJS also includes a number of syntactic conveniences (and some entirely new features), which you will likely want to use to make your code even more expressive compared to JavaScript.

As a small example, in idiomatic SJS we would probably write the loop in the above code differently:

// instead of:
var chapters = transform.par(story.chapterUrls, getJSON);
each(chapters, function(chapter) {
  addHtmlToPage(chapter.html);
});

// we might write:
story.chapterUrls .. transform.par(getJSON) .. each {
  |chapter|
  addHtmlToPage(chapter.html);
}

Here we've used two syntactic features of StratifiedJS: the .. (double-dot) operator to chain together multiple function calls, and the blocklambda syntax (which is a lot like ruby's blocks). In the second version, the code reads "left-to-right" as a single chain of operation, and we didn't need to create intermediate variables in order to break up the expression. We won't go into detail on these here, but please read the linked documentation pages if you're curious about how they work.

Conductance - StratifiedJS in practice

Of course, the ability of StratifiedJS to express asynchronous code in a synchronous style is not its only benefit. Conductance is our end-to-end solution for building rich web applications using StratifiedJS. SJS already runs on both NodeJS and browsers (modern and not-so-modern), but Conductance provides a super-productive server, UI & API framework on top of this, allowing you to build StratifiedJS-based applications in next to no time.

Full demo app

We've put together a version of the entire demo app from the html5rocks article. You can try it out here, and see the full source below:

For comparison, here is the full code for the original promise-based demo app (which you can also try out here):

We haven't changed what the demo code does, but we have changed how it works when there are obvious improvements that Conductance encourages - so for example instead of a separate .css file, we've opted to put all the UI-based content into the ui.sjs module, encapsulating both markup and (reusable) styles. You can of course use regular CSS files in Conductance if you prefer, but using CSS objects usually ends up being a more modular way to manage styles, because you manage their application in your code, rather than by orchestrating global IDs and class names. As in the previous snippet, we've also made idiomatic use of some StratifiedJS specific syntax, including:

  • the .. (double-dot) operator, to chain functions together
  • blocklambda syntax, for anonymous functions that look and act like language blocks
  • the @ (alternate namespace) prefix, which is (by convention) where objects and functions exported from other modules are placed.

We've also left in the niceties that you get by default from Conductance generated HTML, like twitter bootstrap styling, JavaScript error reporting, and a loading indicator. If you're trying to slim down on page size (and code size) though, you can pick a more minimal template (or even roll your own). To minimise server round-trips for the StratifiedJS modules we use (served as distinct .sjs files by default), we've bundled them into a single bundle.js, using StratifiedJS' builtin bundle tool.

The bigger picture

The full demo code shows that while promises are good for combining and sequencing tasks, StratifiedJS' builtin support for the concept of cancellation makes managing inter-related tasks drastically simpler, and can be used to build reusable, modular code. What's interesting is that we didn't cherry-pick this demo in order to show off these attributes - they just appeared naturally when we ported the code.

While translating the promise-based code, we noticed that when you change the "Fake network delay" checkbox (to enable or disable simulation of a slow network), the original code saves this setting in local storage, and then reloads the entire document with document.location.reload(). In a real application, this method of cancellation is obviously unacceptable. And yet, it's not clear how to do it properly. We could insert a hook into the get() function to allow all outstanding requests to be cancelled, but:

  • this only works for outstanding requests made via get(), not other kinds of asynchronous code (or other sources of XMLHttpRequests)
  • it provides no granularity - we'd be cancelling all requests, even ones that are unrelated to the thing we're trying to cancel
  • we'd have to add some kind of "cancelled" return type to get(), and make the rest of our code return early as soon as it gets a "cancelled" result
  • we'd also need to remove any DOM content that was inserted by the partially-run code

In StratifiedJS, we get cancellation for any kind of task, scoped at the language level using the waitfor/or construct. Here's how we've used it in the withFakeNetwork(block) function of util.sjs:

var initialValue = localStorage.getItem(lsKey) === 'true';
var fakeNetworkDelay = ObservableVar(initialValue);
mainContent .. appendContent(fakeNetworkElem) {||

  // run the following block for each new value of `fakeNetworkDelay`
  fakeNetworkDelay .. each {|delay|
    localStorage.setItem(lsKey, String(delay));
    fakeDelay = delay ? 1 : 0;

    waitfor {
      block();
    } or {
      // abort the above block immediately when
      // fakeNetworkDelay gets a new value
      fakeNetworkDelay .. @changes() .. @first();
    }
  }
}

And with that, we've turned the fake network toggle into a modular component - it doesn't need to reload the page (and disrupt all other running code) just to cancel a task. At the language level, any suspended expression inside block() will automatically be retracted, and will have a chance to clean up any open resources by wrapping such code in a try/finally block, or the special-purpose try/retract block (for cleanup code that should only run on cancellation). In this demo, that means:

  • outstanding HTTP requests: the get() function (in util.sjs) uses a try/retract block to abort the request if the call to get() is retracted before the request completes
  • active UI: we use the appendContent(content, block) function (from Conductance's surface UI module) to add content to the document. Surface ensures that content is only shown for the duration of block - when block ends or is cancelled, content is automatically removed from the document

In this demo we would have to go a little further to eliminate global state completely (e.g. the module-level fakeDelay variable in util.sjs), but we've already come a long way towards being self-contained and re-runnable with the simple use of cancellation.


Gratuitous examples:

To get you more comfortable with how StratifiedJS works, it's useful to see some examples of how you would implement the same task in callback-based JavaScript, promise-based JavaScript and StratifiedJS.

We think the below examples are fairly idiomatic for each approach, but let us know in the comments if you think we've missed something or used a poor approach for any of these examples.

Plumbing:

Both SJS and promises require a bit of plumbing to wrap a native (callback-based) function. In SJS, this is done once, and then you can forget about it (the wrapper function acts just like any normal SJS function). With promises, a wrapped function will always return a promise, which means that all callers must use the promise API to make use of returned values.

Callback-based JavaScript
var divideSlowly(a, b, done) {
  window.setTimeout(function() {
    var rv, err;
    try {
      rv = a/b;
    } catch(e) {
      err = e;
    }
    done(err, rv);
  }, 500);
}

// usage: divideSlowly(1, 2, function(err, result) { ... });
Promise wrapper
var divideSlowlyPromise = function(a, b) {
  return new Promise(function(resolve, reject) {
    divideSlowly(a, b, function(err, rv) {
      if (err) reject(err);
      else resolve(rv);
    });
  });
}

// usage: divideSlowlyPromise(1, 2).then(function(half) { ... })
StratifiedJS wrapper
var divideSlowlySJS = function(a, b) {
  waitfor(var err, rv) {
    divideSlowly(a, b, resume);
  }
  if(err) throw err;
  return rv;
}

// usage: var half = divideSlowlySJS(1,2);

Interoperating with promises:

Since promises are making their way into JavaScript APIs, it's also useful to be able to convert a promise into a plain StratifiedJS value. This is even easier than wrapping a callback-based API, since you can use the same function to "unwrap" any promise object, rather than having to write a wrapper for each API function:

Convert a promise into a blocking StratifiedJS value
var toValue = function(promise) {
  waitfor(var err, rv) {
    promise.then(function(success) {
      resume(null, success);
    }, function(err) {
      resume(err);
    });
  }
  if (err) throw err;
  return rv;
}

// usage: console.log(getPromise() .. toValue);

On the flipside, you may need to wrap a StratifiedJS compuation into a promise if you're dealing with JavaScript APIs that only accept promises. Here's how to do that, should you need to:

Convert a StratifiedJS function call into a promise
// (only useful if you need to work with plain JS
// libraries that require promises)
var makePromise = function(fn) {
  return new Promise(function(resolve, reject) {
    try {
      resolve(fn());
    } catch(e) {
      reject(e);
    }
  });
}

// usage: var promise = makePromise(-> get(url));

Managing multiple concurrent tasks:

As well as interoperating with plain JavaScript, concurrency mechanisms need to be able to work with multiple concurrent operations (it's right there in the name). Here are some examples of how you would arrange to do multiple things at the same time and coordinate their results with callback-based code, promise-based code, and StratifiedJS:

Performing multiple operations and waiting for all results:

Callback-based JavaScript
var remaining=2;
var a, b;
var done = function() {
  console.log("a = " + a + ", b = " + b);
};
var doneOne = function() {
  if (--remaining == 0) done();
};

getA(function(_a) {
  a = _a;
  doneOne();
});

getB(function(_b) {
  b = _b;
  doneOne();
});

The above code doesn't handle errors (for some semblance of brevity), but the below snippets both do:

Promises
Promise.all([ getA(), getB() ]).then(function(results) {
  var a = results[0], b = results[1];
  console.log("a = " + a + ", b = " + b);
});
StratifiedJS
waitfor {
  var a = getA();
} and {
  var b = getB();
}
console.log("a = " + a + ", b = " + b);

Wait for the first of inter-related tasks

Callback-based JavaScript
function download(url, done) {
  var cancelled = false;
  var cancelDialog = createCancelDialog("Downloading ...");
  var req = startRequest(url);

  req.on('complete', function(err, result) {
    // if the user cancelled the download, disregard this result
    if (cancelled) return;
    req = null;

    // remove the cancel dialog if it's still being shown
    if (cancelDialog) {
      cancelDialog.close();
      cancelDialog = null;
    }
    done(err, result);
  });

  // show a dialog to the user, allowing them
  // to cancel the operation:
  cancelDialog.on('cancel', function() {
    cancelled = true;
    cancelDialog = null;

    // if we have an outstanding request, cancel it:
    if (req) {
      req.abort();
      req = null;
    }
    // return null in the case of cancellation
    done(null, null);
  });
}
Promises
function download(url) {
  var cancelled = false;
  var Cancel = new Error("Cancelled");
  var result;

  var ignoreCancellation = function(err) {
    if (err === Cancel) return null;
    throw e;
  };

  var download = get(url).catch(ignoreCancellation);
  var dialog = showCancelDialog("Downloading ...").then(function(_) {
    // if the cancel dialog is dismissed, the result of this function
    // should be `null`
    return null;
  }).catch(ignoreCancellation);

  var tasks = [download, dialog];
  function cancelTasks() {
    // send each task a Cancel exception. get() should abort any
    // pending request on error, and showCancelDialog() should
    // remove the dialog on error.
    download.reject(Cancel);
    dialog.reject(Cancel);
  }

  return Promise.race(tasks).then(function(success) {
    cancelTasks();
    return success;
  }, function(err) {
    cancelTasks();
    throw err;
  });

  return result;
}
StratifiedJS
function download(url) {
  waitfor {
    // if this branch finishes first, showCancelDialog()
    // will be cancelled, removing the dialog from the display
    return get(url);
  } or {
    // if this branch finishes first, get(url)
    // will be cancelled (which will cause it
    // to abort any outstanding request)
    showCancelDialog("Downloading ...");
    return null;
  }
}

Hopefully this has given you a taste of how StratifiedJS makes your code simpler and more robust by providing builtin constructs for managing concurrency. If you'd like to learn more, you should check out Conductance, our nodejs server and realtime web framework built around StratifiedJS.


The code samples in this article are licensed under the Apache-2.0 licence, as in the original promises article.