Tab Completion

I'm Tab Atkins Jr, and I wear many hats. I work for Google on the Chrome browser as a Web Standards Hacker. I'm also a member of the CSS Working Group, and am either a member or contributor to several other working groups in the W3C. You can contact me here.
Listing of All Posts

Explaining Futures

Last updated:

DOM Futures were only recently introduced, and it seems like suddenly every API is being forced into using them. Why? How did this crazy idea suddenly appear? Where was the discussion? Hopefully I can answer a few of these questions, and explain why Futures are so useful and should be used widely.

Future History

First of all, Futures didn't spring fully-formed from Anne van Kesteren's head. If you've been doing JS programming the last several years with any of the popular frameworks, you've probably already used Futures, though they might have been called "Promises", "Deferreds", or "Thenables".

Specifically, Futures are based on the Promises/A+ version, standardized by Domenic Denicola and others. Over the last several years, this has turned out to be the most popular and most technically worthy version of Promises.

About two years ago, Alex Russell gathered together several influential and super-smart people with experience in modern webdev, like Erik Arvidsson, Yehuda Katz, Jake Archibald, and others, and started iterating on a proposal to do Promises on the web. While doing so, he also kept some awesome old-hand programming language designers, like Mark Miller, in the loop so that obvious pitfalls and historical errors could be avoided. The final API ended up being Promises/A+ compatible, and had a very sharp, small API that still represented tons of power. This was then ported over into the DOM spec by Anne as DOM Futures.

In short, this isn't some half-baked idea put together in five minutes. It's based on years of real-world experience and lots of thought from really smart hackers.

Future Value

What exactly do Futures bring to the table? Why are they better than Events, or just callbacks?

There are lots of reasons to use some form of callback-based asynchronous API, but one popular reason is because you have some task that you don't want to block the main thread on (maybe it requires file or network IO, for example), and you want to be able to return its value when it finishes.

This sort of pattern shows up everywhere in DOM and related APIs. Unfortunately, it's implemented in a myriad of ways. Some APIs return a dummy object, and then fire a DOM Event at it when the operation is finished. Others fire a DOM Event at an appropriate global interface, with some information to figure out what thing the event was for. Others return a dummy object with a few callback-registering functions (similar to, but not using, DOM Events). Others take callbacks directly in the argument list. Others take callbacks in an options object.

And then, with all the above methods, APIs may or may not have some way to detect errors in the operation, which increases the complexity further. APIs may or may not have an easy way for multiple functions to be registered for the event. APIs may or may not have a way for code to get at the value of the operation after it completes.

All of this adds up to a metric fuckton of API surface for something that is a very simple, small set of meaningful API concepts.

The core value of Futures is that it unifies all of these into a single, idiomatic pattern. Whenever a function will kick off an async task, it should return a Future. The user can then register "accept" or "reject" callbacks on the Future, which get called when the operation completes successfully or fails with an error, respectively. You can register callbacks multiple times, and they'll all be called as appropriate. After the operation completes, you can still register callbacks on the future, and it'll just call them "immediately" (next tick) with the completion value or rejection reason, just like if they were registered before it completed.

"But Tab!", I hear you say, "you could just as easily return an EventTarget object and just fire events! That would accomplish the same thing, but without inventing something new and inconsistent with the rest of the platform!". (Or equivalently, we could standardize some callback argument pattern, or something else.)

You're right, we could! If this was all that Futures offered, they would be a much more difficult sell, and probably not worth the effort.

However, the fact that Futures capture this pattern in a first-class value means that we can push more power into the abstraction. Futures are fundamentally better than the existing patterns for at least 5 strong reasons.

Reason 1: Chaining

Most of the API patterns I rattled off earlier have no convenient way to chain operations. That is, you can't easily schedule a second function to run after your first callback finishes. Most of the time, you have to roll your own chaining somehow, like registering an anonymous function that wraps your two pieces of code.

Futures make this trivial - the return value of .then() (the callback registration function) is another Future, which completes when the callback is finished with the callback's return value. (Actually it's even better - see the next section.) This means you can chain a second function just by calling .then() on this returned value! It's even simpler than it sounds:

someAsyncFunc().then(cb1).then(cb2);

Tada!

Reason 2: More Chaining

There's another type of chaining that is prevalent in async code, which is deeply hated - NESTING HELL. This happens when the first callback finishes up by kicking off another async operation, so you have to pass in another callback to handle that. If you're coding with a lot of anonymous functions (which is totally reasonable, because you're basically just splitting up your literal code into little async chunks), these callbacks end up marching ever rightward, making it difficult to read and understand your program flow.

Futures make this sort of thing trivial, again. Remember in the last section, where I said that the future returned by .then() (let's call this Future2 - you'll see why) completed when the callback returned, with the callback's return value? That's true, but there's some additional magic involved - if the callback returns a future (Future3), then Future2 slaves itself to Future3's state. It waits to resolve until Future3 resolves, and then adopts the same state, accepting or rejecting with the same value. This means that you can do async chaining without the horrible leftward march!

asyncFunc().then(function(val) {
    return anotherAsyncFunc();
  }).then(function(val) {
    // only runs when anotherAsyncFunc()'s operation finishes!
    return yetAnotherAsyncFunc();
  }).then(function(val) {
    // only runs when yetAnotherAsyncFunc()'s operation finishes
  });

Reason 3: Linear callback growth

For those API variants that let you register both success and error callbacks, the lack of good chaining (see reasons 1 and 2) meant that the number of necessary callbacks would grow exponentially:

oldAsyncFunc(function(success) {
    return anotherAsync(function(success) {
        ...
      }, function(error) {
        ...
      });
  }, function(error) {
    return yetAnotherAsync(function(success) {
        ...
      }, function(error) {
        ...
      });
  });

That's 6 function declarations - 2 at the top level, 4 nested - and it would be 14 if the same pattern continued one more level. Of course, in practice by this point people are forced into using named functions, just to avoid all the pointless duplication.

In the reasonably common case where the error function is returning some appropriate default value of the same type as the success function, Futures simplify this, so that you don't have to repeat yourself. This means that using anonymous functions stays viable:

newAsyncFunc().then(function(success) {
    return anotherAsync();
  }, function(error) {
    return default;
  }).then(function(success) {
    return yetAnotherAsync();
  }, function(error) {
    return default2;
  });

This code has only four function declarations, 2 per level. If it went another level deep, it would only be 6. There's no duplication necessary at all.

Reason 4: Errors are easy to deal with

In normal async code, errors are the devil. Throwing errors and using callbacks simply do not mix, because where you going to put the try/catch? You can't put it around the function that takes the callbacks, because it successfully returns immediately. You can't put it around the callbacks, because they're not called until later. The only way to mix them is to invent your own complex wrapper system, and always use it.

Futures takes care of all this for you, in the same trivial way it does the rest. Recall from Reasons 1 and 2 that whenever you attach a callback to a future, it returns a brand new future that resolves to the callback's return value, to make chaining easier. Like all futures, this brand new future can take both accept and reject callbacks, and for good reason - if the callback throws, it'll get caught by the future and just trigger the reject callback automatically!

Reason 5: Future combinators

One of the most annoying things to handle when doing async code (besides chaining, more chaining, and errors) is synchronization. If you have a single async operation you want to run some code after, that's fine. If you have two async operations, and you want to wait until they both finish and call some function with both of their results, you're on your own. You have to manually roll some synchronization primitives, where you pass different dummy functions to both, which use some communication channel to tell each other when they're finished and what their return value was, so the last one to finish can finally call your function. If you have two async operations and you just want to respond to the first of them, you've got to do that stuff all over again, just with slightly different synchronization code.

Once again, futures makes this trivial. The Future interface has several static functions defined on it which combine futures together into new futures:

  • Future.all(): if all the passed futures accept, the output future accepts, with an array of their values. If any of them rejects, the output future rejects with its reason.
  • Future.some(): if any of the passed futures accepts, the output future accepts with its value. If they all reject, the output future rejects with an array of their reasons.
  • Future.any(): whichever of the passed futures is first to accept or reject, the output future does the same with the same value.

For example, if we assume there's a future-returning XHR API, and you want to make two separate XHRs and run some code when they both return, it's easy:

Future.all(getJSON(url1), getJSON(url2)).then(function(arr) {
    // do stuff with arr[0] and arr[1], the JSON results
  }, function(error) {
    // handle the error
  });

Doing the same with today's XHR code, or even jQuery's older (non-Deferred-based) XHR APIs, is non-trivial, but a fun exercise to try if you've never had to do it before. It stops being fun the dozenth time you have to rewrite it in production code, though.

There are potentially even more useful combining operations, but these three should cover the majority of cases.

Future Conclusion

Hopefully, this has enlightened you as to why DOM chose to add Futures, and why some of us are so excited and eager to use Futures everywhere in APIs.

In the DOM world and other closely-related APIs, we're not going to stop using futures, and the value of futures grows even more as more specs use them, due to network effects and developer interest. Please do the right thing, and use Futures when appropriate in your own APIs. Ask us (on www-dom@w3.org) if you're not sure whether or not your API should use Futures, or how best to employ them.

(a limited set of Markdown is supported)

#1 - Niloy Mondal:

In this example: Future.all(getJSON(url1), getJSON(url2)).then(function(arr) {

Is it not better to have the callback function get seperate parameter for each returned value instead of a single array, like so: Future.all(getJSON(url1), getJSON(url2)).then(function(json1, json2) {

Reply?

(a limited set of Markdown is supported)

Re #1: The persistent "pass an array or multiple arguments" dilemna is resolved in the newest version of Javascript, with its rest arguments, spread operator, and argument destructuring.

If you want to recieve the two arguments directly, just do "function([json1, json2]){...}". This automatically pulls the passed array apart into two named arguments for you.

Reply?

(a limited set of Markdown is supported)

#3 - Niloy Mondal:

Ok, but ES6 is not even finalized yet. And even after it does, it will take a few more years to be able to use it on the web. While this 'Future' library is something we can use now.

What is the gain in having it as a single array instead of multiple arguments?

Reply?

(a limited set of Markdown is supported)

Re #3: The gain is that you can easily get all the values, without having to mess around with the arguments object.

It's easy to pull the values out once you have it, and then you can go about your day. The two methods are functionally equivalent, just more convenient for different types of things.

Reply?

(a limited set of Markdown is supported)

#5 - Niloy Mondal:

I agree both are functionally equivalent, that is why the API should be designed to cater to the most commonly occurring pattern. And I really see myself using separate parameters all the time. I cant think of any case where array would be better. Even if there is a case, it would be in minority or for library developers.

Having a single array would force me to extract out the values all the time. It isnt a problem for a POC, but for day-to-day programming, it will become a bit annoying.

Also, having multiple parameters is good for readability as you dont need to look inside the function code to determine what arguments are expected aka the signature of the function. This will be good for named functions.

Ofcourse, all of this is my opinion. I am not trying to annoy you, I am as excited about Futures as you are. My last comment on this topic, thanks for the 'Future' :) .

Reply?

(a limited set of Markdown is supported)

Re #5:

The issue is that promises represent a single value, just like function calls can only return a single value. Having them represent multiple values is nonsensical, so instead combinators like Future.every or Q.all return a promise for an array.

The real confusion is the Future static combinators accept variadic arguments as values, instead of arrays. This is not entirely clear to me.

For more information on the parallel between promises and normal function calls, see "You're Missing the Point of Promises".

Reply?

(a limited set of Markdown is supported)

This is such a recognizable problem. I have even built various portions of this pattern to lessen my frustrations (I am sure a lot of people have come up with something like Future.all before in their projects).

With this version I especially like how you can do:

Future.all(someAsyncFunc().then(cb1), anotherAsyncFunc()).then(cb2);
Reply?

(a limited set of Markdown is supported)